Предварително компилирайте Regex Patterns In Pattern Objects

1. Общ преглед

В този урок ще видим предимствата на предварителното компилиране на модел на регулярно изражение и новите методи, въведени в Java 8 и 11 .

Това няма да е ръководство за регулярни изрази, но за тази цел имаме отличен API за Java Regular Expressions API.

2. Ползи

Повторното използване неизбежно носи повишаване на производителността, тъй като не е необходимо да създаваме и пресъздаваме копия на едни и същи обекти от време на време. Така че, можем да предположим, че повторното използване и ефективността често са свързани.

Нека да разгледаме този принцип, тъй като той се отнася до Pattern # compile. 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 (стойност) .matches ()
  • Предварително компилиран регекс с един екземпляр на Matcher и много извиквания към matcherFromPreCompiledPattern.reset (value) .matches ()

Всъщност, ако разгледаме реализацията на String # :

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

И в Pattern # match :

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 (стойност) .matches ()
    • 1 Екземпляр на шаблон е създаден
    • Създадени са 5 000 000 екземпляра на Matcher
  • matcherFromPreCompiledPattern.reset (стойност) .matches ()
    • 1 Екземпляр на шаблон е създаден
    • 1 Matcher например създаден

Така че, вместо да делегираме нашето регулярно изражение на 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 за останалите.