1. Обзор
В этом руководстве мы увидим преимущества предварительной компиляции шаблона регулярного выражения и новые методы, представленные в Java 8 и 11 .
Это не будет практическим руководством по регулярным выражениям, но для этой цели у нас есть отличное руководство по API регулярных выражений Java .
2. Преимущества
Повторное использование неизбежно приводит к повышению производительности, поскольку нам не нужно время от времени создавать и воссоздавать экземпляры одних и тех же объектов. Таким образом, мы можем предположить, что повторное использование и производительность часто связаны.
Давайте рассмотрим этот принцип применительно к Pattern#compile.
Мы будем использовать простой тест :
- У нас есть список из 5 000 000 номеров от 1 до 5 000 000
- Наше регулярное выражение будет соответствовать четным числам
Итак, давайте проверим синтаксический анализ этих чисел с помощью следующих выражений регулярных выражений Java:
String.matches(regex)
Pattern.matches(regex, charSequence)
Pattern.compile(regex).matcher(charSequence).matches()
- Предварительно скомпилированное регулярное выражение с множеством вызовов
preCompiledPattern.matcher(value).matches()
- Предварительно скомпилированное регулярное выражение с одним экземпляром
Matcher
и множеством вызововmatcherFromPreCompiledPattern.reset(value).matches()
На самом деле, если мы посмотрим на реализацию String#
matches:
public boolean matches(String regex) {
return Pattern.matches(regex, this);
}
И в Pattern#matches
:
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));
}
}
Глядя на результаты тестов, можно не сомневаться, что предварительно скомпилированный Pattern
и повторно используемый Matcher
являются победителями с результатом более чем в шесть раз быстрее :
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
preCompiledPattern.matcher(value).matches()
Создан 1 экземпляр
шаблона
Создано 5 000 000 экземпляров
Matcher
matcherFromPreCompiledPattern.reset(value).matches()
Создан 1 экземпляр
шаблона
Создан 1 экземпляр
Matcher
Итак, вместо того, чтобы делегировать наше регулярное выражение String#matches
или Pattern#matches
, которые всегда будут создавать экземпляры Pattern
и Matcher .
Мы должны предварительно скомпилировать наше регулярное выражение, чтобы повысить производительность и создать меньше объектов.
Чтобы узнать больше о производительности регулярных выражений, ознакомьтесь с нашим обзором производительности регулярных выражений в Java.
3. Новые методы
С появлением функциональных интерфейсов и потоков повторное использование стало проще.
Класс Pattern
эволюционировал в новых версиях Java , чтобы обеспечить интеграцию с потоками и лямбда-выражениями.
3.1. Ява 8
В Java 8 появились два новых метода: splitAsStream
и asPredicate
.
Давайте посмотрим на некоторый код для splitAsStream
, который создает поток из заданной входной последовательности вокруг совпадений с шаблоном:
@Test
public void givenPreCompiledPattern_whenCallSplitAsStream_thenReturnArraySplitByThePattern() {
Pattern splitPreCompiledPattern = Pattern.compile("__");
Stream<String> textSplitAsStream = 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]);
}
Метод asPredicate
создает предикат, который ведет себя так, как будто он создает сопоставитель из входной последовательности, а затем вызывает find:
string -> matcher(string).find();
Давайте создадим шаблон, который соответствует именам из списка, в котором есть как минимум имя и фамилия, содержащие не менее трех букв:
@Test
public void givenPreCompiledPattern_whenCallAsPredicate_thenReturnPredicateToFindPatternInTheList() {
List<String> namesToValidate = Arrays.asList("Fabio Silva", "Mr. Silva");
Pattern firstLastNamePreCompiledPattern = Pattern.compile("[a-zA-Z]{3,} [a-zA-Z]{3,}");
Predicate<String> patternsAsPredicate = firstLastNamePreCompiledPattern.asPredicate();
List<String> validNames = namesToValidate.stream()
.filter(patternsAsPredicate)
.collect(Collectors.toList());
assertEquals(1,validNames.size());
assertTrue(validNames.contains("Fabio Silva"));
}
3.2. Ява 11
В Java 11 появился метод asMatchPredicate
, который создает предикат, который ведет себя так, как если бы он создавал сопоставитель из входной последовательности, а затем вызывал совпадения:
string -> matcher(string).matches();
Давайте создадим шаблон, который соответствует именам из списка, в котором есть только имя и фамилия, содержащие не менее трех букв в каждом:
@Test
public void givenPreCompiledPattern_whenCallAsMatchPredicate_thenReturnMatchPredicateToMatchesPattern() {
List<String> namesToValidate = Arrays.asList("Fabio Silva", "Fabio Luis Silva");
Pattern firstLastNamePreCompiledPattern = Pattern.compile("[a-zA-Z]{3,} [a-zA-Z]{3,}");
Predicate<String> patternAsMatchPredicate = firstLastNamePreCompiledPattern.asMatchPredicate();
List<String> 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
для фрагментов JDK 11 и core-java-regex
для остальных.