Pré-compiler les modèles Regex en objets de modèle

1. Vue d'ensemble

Dans ce didacticiel, nous verrons les avantages de la précompilation d'un modèle regex et les nouvelles méthodes introduites dans Java 8 et 11 .

Ce ne sera pas un tutoriel regex, mais nous avons un excellent Guide de l'API Java Regular Expressions à cet effet.

2. Avantages

La réutilisation apporte inévitablement un gain de performances, car nous n'avons pas besoin de créer et de recréer des instances des mêmes objets à chaque fois. Nous pouvons donc supposer que la réutilisation et les performances sont souvent liées.

Jetons un œil à ce principe en ce qui concerne la compilation de Pattern #. Nous utiliserons un benchmark simple :

  1. Nous avons une liste de 5 000 000 de nombres de 1 à 5 000 000
  2. Notre regex correspondra à des nombres pairs

Alors, testons l'analyse de ces nombres avec les expressions regex Java suivantes:

  • String.matches (regex)
  • Pattern.matches (regex, charSequence)
  • Pattern.compile (regex) .matcher (charSequence) .matches ()
  • Regex pré-compilé avec de nombreux appels à preCompiledPattern.matcher (value) .matches ()
  • Regex pré-compilé avec une matcher instance et de nombreux appels à matcherFromPreCompiledPattern.reset (valeur) .matches ()

En fait, si nous regardons l' implémentation de String # matches :

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

Et aux correspondances de Pattern # :

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

Ensuite, nous pouvons imaginer que les trois premières expressions fonctionneront de la même manière. C'est parce que la première expression appelle la seconde et la seconde appelle la troisième.

Le deuxième point est que ces méthodes ne réutilisent pas les instances Pattern et Matcher créées. Et, comme nous le verrons dans le benchmark, cela dégrade les performances d'un facteur six :

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

En regardant les résultats de référence, il ne fait aucun doute que Pattern pré-compilé et Matcher réutilisé sont les gagnants avec un résultat plus de six fois plus rapide :

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

Au-delà des temps de performance, nous avons également le nombre d'objets créés :

  • Trois premières formes:
    • 5.000.000 d' instances de modèle créées
    • 5.000.000 d' instances Matcher créées
  • preCompiledPattern.matcher (valeur) .matches ()
    • 1 instance de modèle créée
    • 5.000.000 d' instances Matcher créées
  • matcherFromPreCompiledPattern.reset (valeur) .matches ()
    • 1 instance de modèle créée
    • 1 instance de Matcher créée

Ainsi, au lieu de déléguer notre expression régulière aux correspondances String # ou Pattern #, cela créera toujours les instances Pattern et Matcher . Nous devons pré-compiler notre regex pour gagner en performance et créer moins d'objets.

Pour en savoir plus sur les performances dans regex, consultez notre Présentation des performances des expressions régulières en Java.

3. Nouvelles méthodes

Depuis l'introduction des interfaces et des flux fonctionnels, la réutilisation est devenue plus facile.

La classe Pattern a évolué dans les nouvelles versions Java pour fournir une intégration avec les flux et les lambdas.

3.1. Java 8

Java 8 a introduit deux nouvelles méthodes: splitAsStream et asPredicate .

Regardons du code pour splitAsStream qui crée un flux à partir de la séquence d'entrée donnée autour des correspondances du modèle:

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

La méthode asPredicate crée un prédicat qui se comporte comme s'il créait un matcher à partir de la séquence d'entrée, puis appelle find:

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

Créons un modèle qui correspond aux noms d'une liste qui ont au moins le prénom et le nom avec au moins trois lettres chacun:

@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 a introduit la méthode asMatchPredicate qui crée un prédicat qui se comporte comme s'il créait un matcher à partir de la séquence d'entrée, puis appelait matches:

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

Créons un modèle qui correspond aux noms d'une liste qui n'a que le prénom et le nom avec au moins trois lettres chacun:

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

Dans ce tutoriel, nous avons vu que l' utilisation de modèles pré-compilés nous apporte des performances bien supérieures .

Nous avons également découvert trois nouvelles méthodes introduites dans JDK 8 et JDK 11 qui nous facilitent la vie .

Le code de ces exemples est disponible à l'adresse over sur GitHub dans core-java-11 pour les extraits JDK 11 et core-java-regex pour les autres.