Попередньо скомпілюйте шаблони регулярних виразів в об'єкти шаблону

1. Огляд

У цьому підручнику ми побачимо переваги попередньої компіляції шаблону регулярних виразів та нових методів, представлених у Java 8 та 11 .

Це не буде правильним виразом, але для цього у нас є чудовий API по Java Regular Expressions API.

2. Переваги

Повторне використання неминуче приносить підвищення продуктивності, оскільки нам не потрібно час від часу створювати та відтворювати екземпляри одних і тих самих об’єктів. Отже, можна припустити, що повторне використання та продуктивність часто пов’язані між собою.

Давайте розглянемо цей принцип, оскільки він стосується компіляції Pattern #. W e'll використовувати простий тест :

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

Отже, давайте протестуємо синтаксичний розбір цих чисел з такими виразами регулярних виразів Java:

  • String.matches (регулярний вираз)
  • Pattern.matches (регулярний вираз, charSequence)
  • Pattern.compile (регулярний вираз) .matcher (charSequence) .matches ()
  • Попередньо скомпільований регулярний вираз із багатьма викликами preCompiledPattern.matcher (value) .matches ()
  • Попередньо скомпільований регулярний вираз з одним екземпляром Matcher і безліччю викликів matcherFromPreCompiledPattern.reset (value) .matches ()

Власне, якщо ми подивимось на реалізацію рядка # # :

public boolean matches(String regex) { return Pattern.matches(regex, this); }

А на збігах Pattern # :

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
  • preCompiledPattern.matcher (значення) .matches ()
    • 1 Створено примірник шаблону
    • Створено 5 000 000 екземплярів Matcher
  • matcherFromPreCompiledPattern.reset (value) .matches ()
    • 1 Створено примірник шаблону
    • 1 Створено примірник збігу

Отже, замість делегування нашого регулярного виразу String # match або Pattern # match, які завжди створюватимуть екземпляри Pattern і Matcher . Нам слід попередньо скомпілювати наш регулярний вираз, щоб заробити продуктивність і мати менше створених об’єктів.

Щоб дізнатися більше про продуктивність у регулярному виразі, перегляньте наш Огляд продуктивності регулярних виразів у Java.

3. Нові методи

З моменту впровадження функціональних інтерфейсів та потоків повторне використання стало простішим.

Клас Pattern перетворився на нові версії Java, щоб забезпечити інтеграцію з потоками та лямбдами.

3.1. Java 8

Java 8 представила два нові методи: splitAsStream та asPredicate .

Давайте розглянемо деякий код для splitAsStream, який створює потік із заданої вхідної послідовності навколо збігів шаблону:

@Test public void givenPreCompiledPattern_whenCallSplitAsStream_thenReturnArraySplitByThePattern() { Pattern splitPreCompiledPattern = Pattern.compile("__"); Stream 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 namesToValidate = 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 представила метод asMatchPredicate, який створює предикат, який поводиться так, ніби створює збіг із вхідної послідовності, а потім викликає збіги:

string -> matcher(string).matches();

Давайте створимо шаблон, який відповідає іменам зі списку, які мають лише ім’я та прізвище, принаймні з трьома літерами:

@Test public void givenPreCompiledPattern_whenCallAsMatchPredicate_thenReturnMatchPredicateToMatchesPattern() { List namesToValidate = 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 для фрагментів JDK 11 та core-java-regex для інших.