1. Información general
En este tutorial, veremos los beneficios de precompilar un patrón de expresiones regulares y los nuevos métodos introducidos en Java 8 y 11 .
Este no será un tutorial de expresiones regulares, pero tenemos una excelente API de Guide To Java Regular Expressions para ese propósito.
2. Beneficios
La reutilización trae inevitablemente una ganancia de rendimiento, ya que no necesitamos crear y recrear instancias de los mismos objetos una y otra vez. Por lo tanto, podemos asumir que la reutilización y el rendimiento a menudo están vinculados.
Echemos un vistazo a este principio en lo que respecta a la compilación del Patrón #. W e'll utilizar una referencia sencilla :
- Tenemos una lista con 5,000,000 números del 1 al 5,000,000
- Nuestra expresión regular coincidirá con números pares
Entonces, probemos analizar estos números con las siguientes expresiones regulares de Java:
- String.matches (regex)
- Pattern.matches (regex, charSequence)
- Pattern.compile (regex) .matcher (charSequence) .matches ()
- Regex precompilado con muchas llamadas a preCompiledPattern.matcher (value) .matches ()
- Regex precompilado con una instancia de Matcher y muchas llamadas a matcherFromPreCompiledPattern.reset (value) .matches ()
En realidad, si miramos la implementación de las coincidencias de String # :
public boolean matches(String regex) { return Pattern.matches(regex, this); }
Y en las coincidencias del Patrón # :
public static boolean matches(String regex, CharSequence input) { Pattern p = compile(regex); Matcher m = p.matcher(input); return m.matches(); }
Entonces, podemos imaginar que las primeras tres expresiones funcionarán de manera similar. Eso es porque la primera expresión llama a la segunda y la segunda llama a la tercera.
El segundo punto es que estos métodos no reutilizan las instancias de Pattern y Matcher creadas. Y, como veremos en el punto de referencia, esto degrada el rendimiento en un factor de seis :
@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)); } }
Al observar los resultados de las pruebas comparativas, no hay duda de que Pattern precompilado y Matcher reutilizado son los ganadores con un resultado seis veces más rápido :
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
Más allá de los tiempos de actuación, también tenemos la cantidad de objetos creados :
- Primeras tres formas:
- 5,000,000 instancias de patrón creadas
- 5,000,000 instancias de Matcher creadas
- preCompiledPattern.matcher (valor) .matches ()
- 1 instancia de patrón creada
- 5,000,000 instancias de Matcher creadas
- matcherFromPreCompiledPattern.reset (valor) .matches ()
- 1 instancia de patrón creada
- 1 instancia de Matcher creada
Por lo tanto, en lugar de delegar nuestra expresión regular a las coincidencias de String # o Pattern # que siempre crearán las instancias de Pattern y Matcher . Debemos precompilar nuestra expresión regular para obtener rendimiento y tener menos objetos creados.
Para saber más sobre el rendimiento en expresiones regulares, consulte nuestra Descripción general del rendimiento de expresiones regulares en Java.
3. Nuevos métodos
Desde la introducción de interfaces y flujos funcionales, la reutilización se ha vuelto más fácil.
La clase Pattern ha evolucionado en nuevas versiones de Java para proporcionar integración con streams y lambdas.
3.1. Java 8
Java 8 introdujo dos métodos nuevos: splitAsStream y asPredicate .
Veamos un código para splitAsStream que crea una secuencia a partir de la secuencia de entrada dada alrededor de coincidencias del patrón:
@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]); }
El método asPredicate crea un predicado que se comporta como si creara un comparador a partir de la secuencia de entrada y luego llama a find:
string -> matcher(string).find();
Creemos un patrón que coincida con los nombres de una lista que tenga al menos nombre y apellido con al menos tres letras cada uno:
@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 introdujo el método asMatchPredicate que crea un predicado que se comporta como si creara un comparador a partir de la secuencia de entrada y luego llama a coincidencias:
string -> matcher(string).matches();
Creemos un patrón que coincida con los nombres de una lista que solo tienen nombre y apellido con al menos tres letras cada uno:
@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. Conclusión
En este tutorial, vimos que el uso de patrones precompilados nos brinda un rendimiento muy superior .
También aprendimos sobre tres nuevos métodos introducidos en JDK 8 y JDK 11 que nos facilitan la vida .
El código para estos ejemplos está disponible en GitHub en core-java-11 para los fragmentos de código JDK 11 y core-java-regex para los demás.