1. Обзор
В этом уроке мы увидим преимущества предварительной компиляции шаблона регулярных выражений и новые методы, представленные в Java 8 и 11 .
Это не будет руководство по регулярному выражению, но у нас есть отличное руководство по API регулярных выражений Java для этой цели.
2. Преимущества
Повторное использование неизбежно приводит к повышению производительности, поскольку нам не нужно раз за разом создавать и воссоздавать экземпляры одних и тех же объектов. Таким образом, мы можем предположить, что повторное использование и производительность часто связаны.
Давайте рассмотрим этот принцип, поскольку он относится к Pattern#compile. W e будет использовать простой бенчмарк :
- У нас есть список с 5 000 000 номеров от 1 до 5 000 000
- Наше регулярное выражение будет соответствовать четным числам
Итак, давайте проверим разбор этих чисел с помощью следующих выражений регулярных выражений Java:
- String.matches(регулярное выражение)
- Pattern.matches(регулярное выражение, последовательность символов)
- Pattern.compile(регулярное выражение).matcher(CharSequence).matches()
- Предварительно скомпилированное регулярное выражение с множеством вызовов preCompiled Pattern.matcher(value).спички()
- Предварительно скомпилированное регулярное выражение с одним экземпляром Matcher и множеством вызовов совпадений из предварительно скомпилированного шаблона.reset(значение).спички()
На самом деле, если мы посмотрим на реализацию String#matches :
public boolean matches(String regex) { return Pattern.matches(regex, this); }
И в Шаблон#соответствует :
public static boolean matches(String regex, CharSequence input) { Pattern p = compile(regex); Matcher m = p.matcher(input); return m.matches(); }
Тогда мы можем представить, что первые три выражения будут работать аналогично. Это потому, что первое выражение вызывает второе, а второе вызывает третье.
Второй момент заключается в том, что эти методы не используют созданные экземпляры Pattern и Matcher . И, как мы увидим в тесте, это снижает производительность в шесть раз :
@Benchmark public void matcherFromPreCompiledPatternResetMatches(Blackhole bh) { for (String value : values) { bh.consume(matcherFromPreCompiledPattern.reset(value).matches()); } } @Benchmark public void preCompiledPatternMatcherMatches(Blackhole bh) { for (String value : values) { bh.consume(preCompiledPattern.matcher(value).matches()); } } @Benchmark public void patternCompileMatcherMatches(Blackhole bh) { for (String value : values) { bh.consume(Pattern.compile(PATTERN).matcher(value).matches()); } } @Benchmark public void patternMatches(Blackhole bh) { for (String value : values) { bh.consume(Pattern.matches(PATTERN, value)); } } @Benchmark public void stringMatchs(Blackhole bh) { Instant start = Instant.now(); for (String value : values) { bh.consume(value.matches(PATTERN)); } }
Глядя на результаты бенчмарка, нет никаких сомнений в том, что предварительно скомпилированный Шаблон и уменьшенные Совпадения являются победителями с результатом более чем в шесть раз быстрее :
Benchmark Mode Cnt Score Error Units PatternPerformanceComparison.matcherFromPreCompiledPatternResetMatches avgt 20 278.732 ± 22.960 ms/op PatternPerformanceComparison.preCompiledPatternMatcherMatches avgt 20 500.393 ± 34.182 ms/op PatternPerformanceComparison.stringMatchs avgt 20 1433.099 ± 73.687 ms/op PatternPerformanceComparison.patternCompileMatcherMatches avgt 20 1774.429 ± 174.955 ms/op PatternPerformanceComparison.patternMatches avgt 20 1792.874 ± 130.213 ms/op
Помимо времени выполнения, у нас также есть количество созданных объектов :
- Первые три формы:
- 5 000 000 Шаблон созданные экземпляры
- 5 000 000 Matcher созданных экземпляров
- Предварительно скомпилированный шаблон.matcher(значение).спички()
- 1 Шаблон экземпляр создан
- 5 000 000 Matcher созданных экземпляров
- совпадения Из предварительно Скомпилированного Шаблона.сброс(значение).спички()
- 1 Шаблон экземпляр создан
- 1 Сопоставитель экземпляр создан
Таким образом, вместо делегирования нашего регулярного выражения String#matches или Pattern#matches , который всегда будет создавать экземпляры Pattern и Matcher . Мы должны предварительно скомпилировать ваше регулярное выражение, чтобы повысить производительность и создать меньше объектов.
Чтобы узнать больше о производительности в регулярных выражениях, ознакомьтесь с нашим обзором производительности регулярных выражений в Java.
3. Новые Методы
С появлением функциональных интерфейсов и потоков повторное использование стало проще.
Класс Pattern эволюционировал в новых версиях Java , чтобы обеспечить интеграцию с потоками и лямбдами.
3.1. Java 8
Java 8 представила два новых метода: splitAsStream и как Предикат .
Давайте рассмотрим некоторый код для splitAsStream , который создает поток из заданной входной последовательности вокруг совпадений шаблона:
@Test public void givenPreCompiledPattern_whenCallSplitAsStream_thenReturnArraySplitByThePattern() { Pattern splitPreCompiledPattern = Pattern.compile("__"); StreamtextSplitAsStream = splitPreCompiledPattern.splitAsStream("My_Name__is__Fabio_Silva"); String[] textSplit = textSplitAsStream.toArray(String[]::new); assertEquals("My_Name", textSplit[0]); assertEquals("is", textSplit[1]); assertEquals("Fabio_Silva", textSplit[2]); }
Метод as Predicate создает предикат, который ведет себя так, как если бы он создавал сопоставитель из входной последовательности, а затем вызывал find:
string -> matcher(string).find();
Давайте создадим шаблон, который соответствует именам из списка, содержащего по крайней мере имя и фамилию с по крайней мере тремя буквами в каждом:
@Test public void givenPreCompiledPattern_whenCallAsPredicate_thenReturnPredicateToFindPatternInTheList() { ListnamesToValidate = Arrays.asList("Fabio Silva", "Mr. Silva"); Pattern firstLastNamePreCompiledPattern = Pattern.compile("[a-zA-Z]{3,} [a-zA-Z]{3,}"); Predicate patternsAsPredicate = firstLastNamePreCompiledPattern.asPredicate(); List validNames = namesToValidate.stream() .filter(patternsAsPredicate) .collect(Collectors.toList()); assertEquals(1,validNames.size()); assertTrue(validNames.contains("Fabio Silva")); }
3.2. Java 11
Java 11 представила предикат as Match метод , который создает предикат, который ведет себя так, как если бы он создавал сопоставитель из входной последовательности, а затем вызывал совпадения:
string -> matcher(string).matches();
Давайте создадим шаблон, который соответствует именам из списка, в котором есть только имя и фамилия, по крайней мере, с тремя буквами в каждой:
@Test public void givenPreCompiledPattern_whenCallAsMatchPredicate_thenReturnMatchPredicateToMatchesPattern() { ListnamesToValidate = Arrays.asList("Fabio Silva", "Fabio Luis Silva"); Pattern firstLastNamePreCompiledPattern = Pattern.compile("[a-zA-Z]{3,} [a-zA-Z]{3,}"); Predicate patternAsMatchPredicate = firstLastNamePreCompiledPattern.asMatchPredicate(); List validatedNames = namesToValidate.stream() .filter(patternAsMatchPredicate) .collect(Collectors.toList()); assertTrue(validatedNames.contains("Fabio Silva")); assertFalse(validatedNames.contains("Fabio Luis Silva")); }
4. Заключение
В этом уроке мы увидели, что использование предварительно скомпилированных шаблонов обеспечивает нам гораздо более высокую производительность .
Мы также узнали о трех новых методах, введенных в JDK 8 и JDK 11, которые облегчают нашу жизнь .
Код для этих примеров доступен на GitHub в core-java-11 для фрагментов JDK11 и core-java-regex для других.