Pre-compileer Regex-patronen in patroonobjecten

1. Overzicht

In deze zelfstudie zien we de voordelen van het vooraf compileren van een regex-patroon en de nieuwe methoden geïntroduceerd in Java 8 en 11.

Dit is geen how-to voor regex, maar daarvoor hebben we een uitstekende Guide To Java Regular Expressions API.

2. Voordelen

Hergebruik levert onvermijdelijk prestatieverbetering op, omdat we niet keer op keer instanties van dezelfde objecten hoeven te maken en opnieuw te creëren. We kunnen dus aannemen dat hergebruik en prestatie vaak met elkaar verbonden zijn.

Laten we dit principe eens bekijken, aangezien het betrekking heeft op Pattern # compileren. W.we zullen een eenvoudige benchmark gebruiken:

  1. We hebben een lijst met 5.000.000 nummers van 1 tot 5.000.000
  2. Onze regex komt overeen met even getallen

Laten we het parseren van deze getallen dus testen met de volgende Java-regex-expressies:

  • String.matches (regex)
  • Pattern.matches (regex, charSequence)
  • Pattern.compile (regex) .matcher (charSequence) .matches ()
  • Voorgecompileerde regex met veel aanroepen naar preCompiledPattern.matcher (waarde) .matches ()
  • Voorgecompileerde regex met één Matcher instantie en veel oproepen naar matcherFromPreCompiledPattern.reset (waarde) .matches ()

Eigenlijk, als we kijken naar de Tekenreeks # komt overeen‘S implementatie:

openbare booleaanse overeenkomsten (String regex) {return Pattern.matches (regex, this); }

En bij Patroon # komt overeen:

openbare statische booleaanse overeenkomsten (String-regex, CharSequence-invoer) {Pattern p = compileren (regex); Matcher m = p.matcher (invoer); retourneer m.matches (); }

Dan kunnen we ons dat voorstellen de eerste drie uitdrukkingen zullen op dezelfde manier presteren. Dat komt doordat de eerste uitdrukking de tweede aanroept en de tweede de derde.

Het tweede punt is dat deze methoden de Patroon en Matcher instanties gemaakt. En, zoals we zullen zien in de benchmark, dit verslechtert de prestaties met een factor zes:

 @Benchmark openbare ongeldige matcherFromPreCompiledPatternResetMatches (Blackhole bh) {voor (Stringwaarde: waarden) {bh.consume (matcherFromPreCompiledPattern.reset (waarde) .matches ()); }} @Benchmark public void preCompiledPatternMatcherMatches (Blackhole bh) {voor (Stringwaarde: waarden) {bh.consume (preCompiledPattern.matcher (waarde) .matches ()); }} @Benchmark public void patternCompileMatcherMatches (Blackhole bh) {for (String-waarde: waarden) {bh.consume (Pattern.compile (PATTERN) .matcher (waarde) .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)); }} 

Kijkend naar de benchmarkresultaten, lijdt het geen twijfel vooraf samengesteld Patroon en hergebruikt Matcher zijn de winnaars met een resultaat van meer dan zes keer sneller:

Benchmark-modus Cnt Score Fout Eenheden PatternPerformanceComparison.matcherFromPreCompiledPatternResetMatches gemiddeld 20278.732 ± 22.960 ms / op PatternPerformanceComparison.preCompiledPatternMatcherMatches gemiddeld 20500.393 ± 34.182 ms / op PatternPerformanceComparison.stringMatches / op een gemiddelde van 20.500.393. PatternPerformanceComparison.patternMatches gemiddelde 20 1792,874 ± 130,213 ms / op

Naast de prestatietijden hebben we ook het aantal gemaakte objecten:

  • Eerste drie vormen:
    • 5,000,000 Patroon instanties gemaakt
    • 5,000,000 Matcher instanties gemaakt
  • preCompiledPattern.matcher (waarde) .matches ()
    • 1 Patroon instantie gemaakt
    • 5,000,000 Matcher instanties gemaakt
  • matcherFromPreCompiledPattern.reset (waarde) .matches ()
    • 1 Patroon instantie gemaakt
    • 1 Matcher instantie gemaakt

Dus in plaats van onze regex te delegeren aan Tekenreeks # komt overeen of Patroon # komt overeen dat altijd de Patroon en Matcher gevallen. We moeten onze regex vooraf compileren om prestaties te verdienen en er minder objecten zijn gemaakt.

Raadpleeg ons Overzicht van prestaties van reguliere expressies in Java voor meer informatie over prestaties in regex.

3. Nieuwe methoden

Sinds de introductie van functionele interfaces en streams is hergebruik eenvoudiger geworden.

De Patroon class is geëvolueerd in nieuwe Java-versies om integratie met streams en lambda's te bieden.

3.1. Java 8

Java 8 introduceerde twee nieuwe methoden: splitAsStream en asPredicate.

Laten we eens kijken naar wat code voor splitAsStream die een stroom creëert van de gegeven invoersequentie rond overeenkomsten van het patroon:

@Test openbare leegte gegevenPreCompiledPattern_whenCallSplitAsStream_thenReturnArraySplitByThePattern () {Pattern splitPreCompiledPattern = Pattern.compile ("__"); Stream textSplitAsStream = splitPreCompiledPattern.splitAsStream ("Mijn_naam__is__Fabio_Silva"); String [] textSplit = textSplitAsStream.toArray (String [] :: nieuw); assertEquals ("My_Name", textSplit [0]); assertEquals ("is", textSplit [1]); assertEquals ("Fabio_Silva", textSplit [2]); }

De asPredicate methode maakt een predikaat aan dat zich gedraagt ​​alsof het een matcher maakt uit de invoerreeks en roept vervolgens find:

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

Laten we een patroon maken dat overeenkomt met namen uit een lijst die ten minste voor- en achternaam hebben met elk ten minste drie letters:

@Test openbare ongeldig gegevenPreCompiledPattern_whenCallAsPredicate_thenReturnPredicateToFindPatternInTheList () {List namesToValidate = Arrays.asList ("Fabio Silva", "Mr. Silva"); Pattern firstLastNamePreCompiledPattern = Pattern.compile ("[a-zA-Z] {3,} [a-zA-Z] {3,}"); Predicaat patronenAsPredicate = firstLastNamePreCompiledPattern.asPredicate (); Lijst validNames = namesToValidate.stream () .filter (patternsAsPredicate) .collect (Collectors.toList ()); assertEquals (1, validNames.size ()); assertTrue (validNames.contains ("Fabio Silva")); }

3.2. Java 11

Java 11 introduceerde het asMatchPredicate methode dat een predikaat creëert dat zich gedraagt ​​alsof het een matcher maakt op basis van de invoersequentie en vervolgens matches aanroept:

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

Laten we een patroon maken dat overeenkomt met namen uit een lijst die alleen voor- en achternaam hebben met elk minstens drie letters:

@Test openbare ongeldig gegevenPreCompiledPattern_whenCallAsMatchPredicate_thenReturnMatchPredicateToMatchesPattern () {List namesToValidate = Arrays.asList ("Fabio Silva", "Fabio Luis Silva"); Pattern firstLastNamePreCompiledPattern = Pattern.compile ("[a-zA-Z] {3,} [a-zA-Z] {3,}"); Predicaat patternAsMatchPredicate = firstLastNamePreCompiledPattern.asMatchPredicate (); Lijst validatedNames = namesToValidate.stream () .filter (patternAsMatchPredicate) .collect (Collectors.toList ()); assertTrue (validatedNames.contains ("Fabio Silva")); assertFalse (validatedNames.contains ("Fabio Luis Silva")); }

4. Conclusie

In deze tutorial hebben we gezien dat de het gebruik van voorgecompileerde patronen levert ons een veel betere prestatie op.

We leerden er ook over drie nieuwe methoden geïntroduceerd in JDK 8 en JDK 11 die ons leven gemakkelijker maken.

De code voor deze voorbeelden is beschikbaar op GitHub in core-java-11 voor de JDK 11-fragmenten en core-java-regex voor de anderen.