«1. Обзор
В этом руководстве мы увидим преимущества предварительной компиляции шаблона регулярного выражения и новые методы, представленные в Java 8 и 11.
Это не будет практическим руководством по регулярному выражению, но у нас есть отличное руководство по API регулярных выражений Java для этой цели.
2. Преимущества
Повторное использование неизбежно приводит к повышению производительности, поскольку нам не нужно время от времени создавать и воссоздавать экземпляры одних и тех же объектов. Таким образом, мы можем предположить, что повторное использование и производительность часто связаны.
Давайте рассмотрим этот принцип применительно к Pattern#compile. Мы будем использовать простой тест:
- We have a list with 5,000,000 numbers from 1 to 5,000,000
- Our regex will match even numbers
Итак, давайте проверим синтаксический анализ этих чисел со следующими выражениями регулярных выражений 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. и экземпляры Matcher. Мы должны предварительно скомпилировать наше регулярное выражение, чтобы повысить производительность и создать меньше объектов.
Чтобы узнать больше о производительности регулярных выражений, ознакомьтесь с нашим обзором производительности регулярных выражений в Java.
3. Новые методы
С появлением функциональных интерфейсов и потоков повторное использование стало проще.
Класс Pattern эволюционировал в новых версиях Java, чтобы обеспечить интеграцию с потоками и лямбда-выражениями.
3.1. Java 8
В Java 8 появились два новых метода: splitAsStream и asPredicate.
Давайте посмотрим на некоторый код для splitAsStream, который создает поток из заданной входной последовательности вокруг совпадений с шаблоном:
Метод asPredicate создает предикат, который ведет себя так, как будто он создает сопоставитель из входной последовательности и затем вызывает find:
@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]);
}
Давайте создадим шаблон, который соответствует именам из списка, в котором есть хотя бы имя и фамилия, каждая из которых состоит не менее чем из трех букв:
string -> matcher(string).find();
3.2. Java 11
@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"));
}
В Java 11 появился метод asMatchPredicate, который создает предикат, который ведет себя так, как если бы он создавал сопоставитель из входной последовательности, а затем вызывал совпадения:
Давайте создадим шаблон, который сопоставляет имена из списка, который только имя и фамилия, каждая из которых состоит не менее чем из трех букв:
string -> matcher(string).matches();
4. Заключение
@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"));
}
В этом руководстве мы увидели, что использование предварительно скомпилированных шаблонов обеспечивает гораздо более высокую производительность.
Мы также узнали о трех новых методах, представленных в JDK 8 и JDK 11, которые облегчают нашу жизнь.
Код для этих примеров доступен на GitHub в core-java-11 для фрагментов JDK 11 и core-java-regex для остальных.
«