Перейти к основному содержимому

Предварительная компиляция шаблонов регулярных выражений в объекты шаблонов

· 5 мин. чтения

Задача: Сумма двух чисел

Напишите функцию twoSum. Которая получает массив целых чисел nums и целую сумму target, а возвращает индексы двух чисел, сумма которых равна target. Любой набор входных данных имеет ровно одно решение, и вы не можете использовать один и тот же элемент дважды. Ответ можно возвращать в любом порядке...

ANDROMEDA

1. Обзор

В этом руководстве мы увидим преимущества предварительной компиляции шаблона регулярного выражения и новые методы, представленные в Java 8 и 11 .

Это не будет практическим руководством по регулярным выражениям, но для этой цели у нас есть отличное руководство по API регулярных выражений Java .

2. Преимущества

Повторное использование неизбежно приводит к повышению производительности, поскольку нам не нужно время от времени создавать и воссоздавать экземпляры одних и тех же объектов. Таким образом, мы можем предположить, что повторное использование и производительность часто связаны.

Давайте рассмотрим этот принцип применительно к Pattern#compile. Мы будем использовать простой тест :

  1. У нас есть список из 5 000 000 номеров от 1 до 5 000 000
  2. Наше регулярное выражение будет соответствовать четным числам

Итак, давайте проверим синтаксический анализ этих чисел с помощью следующих выражений регулярных выражений 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 для остальных.