«1. Обзор

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

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

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

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

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

  1. We have a list with 5,000,000 numbers from 1 to 5,000,000
  2. 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 для остальных.

«