Regex-Muster in Musterobjekte vorkompilieren

1. Übersicht

In diesem Tutorial werden die Vorteile der Vorkompilierung eines Regex-Musters und die in Java 8 und 11 eingeführten neuen Methoden vorgestellt .

Dies ist keine Regex-Anleitung, aber wir haben zu diesem Zweck einen hervorragenden Leitfaden für die Java Regular Expressions-API.

2. Vorteile

Die Wiederverwendung bringt unweigerlich einen Leistungsgewinn mit sich, da wir nicht immer wieder Instanzen derselben Objekte erstellen und neu erstellen müssen. Wir können also davon ausgehen, dass Wiederverwendung und Leistung häufig miteinander verbunden sind.

Werfen wir einen Blick auf dieses Prinzip, das sich auf die Kompilierung von Pattern # bezieht . W e'll eine einfache Benchmark verwenden :

  1. Wir haben eine Liste mit 5.000.000 Zahlen von 1 bis 5.000.000
  2. Unsere Regex entspricht geraden Zahlen

Testen wir also das Parsen dieser Zahlen mit den folgenden Java-Regex-Ausdrücken:

  • String.matches (Regex)
  • Pattern.matches (Regex, charSequence)
  • Pattern.compile (Regex) .matcher (charSequence) .matches ()
  • Vorkompilierter regulärer Ausdruck mit vielen Aufrufen von preCompiledPattern.matcher (Wert) .matches ()
  • Vorkompilierter Regex mit einer Matcher- Instanz und vielen Aufrufen von matcherFromPreCompiledPattern.reset (value) .matches ()

Wenn wir uns die Implementierung der String # -Matches ansehen :

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

Und bei Pattern # -Matches :

public static boolean matches(String regex, CharSequence input) { Pattern p = compile(regex); Matcher m = p.matcher(input); return m.matches(); }

Dann können wir uns vorstellen, dass die ersten drei Ausdrücke ähnlich funktionieren. Das liegt daran, dass der erste Ausdruck den zweiten und der zweite den dritten aufruft.

Der zweite Punkt ist , dass diese Methoden wieder verwenden nicht das Pattern und Matcher Instanzen erstellt. Und wie wir im Benchmark sehen werden, verschlechtert dies die Leistung um den Faktor sechs :

 @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)); } } 

Wenn man sich die Benchmark-Ergebnisse ansieht, besteht kein Zweifel daran, dass vorkompiliertes Muster und wiederverwendeter Matcher die Gewinner mit einem mehr als sechsmal schnelleren Ergebnis sind :

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

Über die Aufführungszeiten hinaus haben wir auch die Anzahl der erstellten Objekte :

  • Die ersten drei Formen:
    • 5.000.000 Musterinstanzen erstellt
    • 5.000.000 Matcher- Instanzen erstellt
  • preCompiledPattern.matcher (Wert) .matches ()
    • 1 Musterinstanz erstellt
    • 5.000.000 Matcher- Instanzen erstellt
  • matcherFromPreCompiledPattern.reset (Wert) .matches ()
    • 1 Musterinstanz erstellt
    • 1 Matcher- Instanz erstellt

Anstatt also unsere Regex Delegieren String # Streichhölzer oder Muster # Begegnungen , die immer das schaffen wird Pattern und Matcher - Instanzen. Wir sollten unseren regulären Ausdruck vorkompilieren, um Leistung zu erzielen und weniger Objekte erstellen zu lassen.

Weitere Informationen zur Leistung in Regex finden Sie in unserer Übersicht über die Leistung regulärer Ausdrücke in Java.

3. Neue Methoden

Seit der Einführung funktionaler Schnittstellen und Streams ist die Wiederverwendung einfacher geworden.

Die Pattern- Klasse wurde in neuen Java-Versionen entwickelt , um die Integration mit Streams und Lambdas zu ermöglichen.

3.1. Java 8

Java 8 führte zwei neue Methoden ein: splitAsStream und asPredicate .

Schauen wir uns einen Code für splitAsStream an , der aus der angegebenen Eingabesequenz einen Stream um Übereinstimmungen des Musters erstellt:

@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]); }

Die asPredicate- Methode erstellt ein Prädikat, das sich so verhält, als würde aus der Eingabesequenz ein Matcher erstellt, und ruft dann find auf:

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

Erstellen wir ein Muster, das mit Namen aus einer Liste übereinstimmt, die mindestens Vor- und Nachnamen mit jeweils mindestens drei Buchstaben enthält:

@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 führte die asMatchPredicate- Methode ein , die ein Prädikat erstellt, das sich so verhält, als würde es einen Matcher aus der Eingabesequenz erstellen und dann Übereinstimmungen aufrufen :

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

Erstellen wir ein Muster, das Namen aus einer Liste mit nur Vor- und Nachnamen mit jeweils mindestens drei Buchstaben entspricht:

@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. Fazit

In diesem Tutorial haben wir gesehen, dass die Verwendung vorkompilierter Muster uns eine weitaus bessere Leistung bringt .

Wir haben auch drei neue Methoden kennengelernt, die in JDK 8 und JDK 11 eingeführt wurden und unser Leben erleichtern .

Der Code für diese Beispiele ist auf GitHub in Core-Java-11 für die JDK 11-Snippets und Core-Java-Regex für die anderen verfügbar .