Precompilare i pattern Regex in oggetti pattern

1. Panoramica

In questo tutorial vedremo i vantaggi della precompilazione di un pattern regex e dei nuovi metodi introdotti in Java 8 e 11 .

Questa non sarà un'espressione regolare, ma per questo scopo abbiamo un'eccellente guida alle API Java Regular Expressions.

2. Benefici

Il riutilizzo porta inevitabilmente a migliorare le prestazioni, poiché non è necessario creare e ricreare istanze degli stessi oggetti di volta in volta. Quindi, possiamo presumere che il riutilizzo e le prestazioni siano spesso collegati.

Diamo un'occhiata a questo principio per quanto riguarda la compilazione Pattern #. W e'll utilizzare un riferimento semplice :

  1. Abbiamo un elenco con 5.000.000 di numeri da 1 a 5.000.000
  2. La nostra regex abbinerà numeri pari

Quindi, proviamo ad analizzare questi numeri con le seguenti espressioni regex Java:

  • String.matches (regex)
  • Pattern.matches (regex, charSequence)
  • Pattern.compile (regex) .matcher (charSequence) .matches ()
  • Regx precompilato con molte chiamate a preCompiledPattern.matcher (value) .matches ()
  • Espressione regolare precompilata con un'istanza di Matcher e molte chiamate a matcherFromPreCompiledPattern.reset (value) .matches ()

In realtà, se guardiamo all'implementazione delle corrispondenze di String # :

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

E alle corrispondenze del modello # :

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

Quindi, possiamo immaginare che le prime tre espressioni funzioneranno in modo simile. Questo perché la prima espressione chiama la seconda e la seconda la terza.

Il secondo punto è che questi metodi non riutilizzano le istanze di Pattern e Matcher create. E, come vedremo nel benchmark, questo riduce le prestazioni di un fattore sei :

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

Guardando i risultati del benchmark, non c'è dubbio che Pattern precompilato e Matcher riutilizzato siano i vincitori con un risultato più di sei volte più veloce :

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

Oltre ai tempi di esecuzione, abbiamo anche il numero di oggetti creati :

  • Prime tre forme:
    • 5.000.000 di istanze di Pattern create
    • 5.000.000 di istanze Matcher create
  • preCompiledPattern.matcher (valore) .matches ()
    • 1 istanza di pattern creata
    • 5.000.000 di istanze Matcher create
  • matcherFromPreCompiledPattern.reset (valore) .matches ()
    • 1 istanza di pattern creata
    • 1 istanza Matcher creata

Quindi, invece di delegare la nostra regex alle corrispondenze String # o Pattern # , creeremo sempre le istanze Pattern e Matcher . Dovremmo pre-compilare la nostra regex per ottenere prestazioni e ha meno oggetti creati.

Per saperne di più sulle prestazioni in regex, consulta la nostra Panoramica delle prestazioni delle espressioni regolari in Java.

3. Nuovi metodi

Dall'introduzione di interfacce e flussi funzionali, il riutilizzo è diventato più facile.

La classe Pattern si è evoluta nelle nuove versioni di Java per fornire l'integrazione con flussi e lambda.

3.1. Java 8

Java 8 ha introdotto due nuovi metodi: splitAsStream e asPredicate .

Diamo un'occhiata ad un po 'di codice per splitAsStream che crea un flusso dalla sequenza di input data attorno alle corrispondenze del modello:

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

Il metodo asPredicate crea un predicato che si comporta come se creasse un matcher dalla sequenza di input e quindi chiama find:

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

Creiamo uno schema che corrisponda ai nomi da un elenco che abbia almeno nome e cognome con almeno tre lettere ciascuno:

@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 ha introdotto il metodo asMatchPredicate che crea un predicato che si comporta come se creasse un matcher dalla sequenza di input e quindi chiama le corrispondenze:

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

Creiamo un modello che corrisponda ai nomi da un elenco che hanno solo nome e cognome con almeno tre lettere ciascuno:

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

In questo tutorial, abbiamo visto che l' uso di pattern precompilati ci offre prestazioni di gran lunga superiori .

Abbiamo anche appreso tre nuovi metodi introdotti in JDK 8 e JDK 11 che ci semplificano la vita .

Il codice per questi esempi è disponibile su GitHub in core-java-11 per gli snippet JDK 11 e core-java-regex per gli altri.