W3docs

Cuantificadores de Regex en Java

Cómo se comportan los cuantificadores regex de Java (*, +, ?, {n,m}) en los modos greedy, reluctant y possessive.

Un cuantificador indica cuántas veces puede repetirse el elemento que le precede. * significa "cero o más", + significa "uno o más", ? significa "cero o uno", y {n,m} especifica un rango exacto. Esto es común a todos los sabores de regex. Lo que confunde a los desarrolladores en Java es que cada cuantificador tiene tres modosgreedy, reluctant y possessive — y es el modo, no el conteo, el que determina cómo el motor de regex en java.util.regex recorre la entrada.

Esta página cubre los cuatro cuantificadores básicos, los tres modos de coincidencia, por qué los patrones greedy coinciden en exceso, y cómo los cuantificadores possessive protegen contra el retroceso catastrófico. Se asume que ya sabes cómo compilar un Pattern y ejecutar un Matcher; si no es así, comienza con Pattern and Matcher. Para los metacaracteres que repetirás, consulta clases de caracteres, y para capturar el texto repetido, ve a grupos.

Los cuatro cuantificadores básicos

Adjunta un cuantificador a un carácter individual, una clase de caracteres o un grupo, y controla la repetición de lo que está inmediatamente a su izquierda:

CuantificadorRepite el elemento precedenteEjemploCoincide con
*cero o más vecesab*cac, abc, abbbc
+una o más vecesab+cabc, abbc (no ac)
?cero o una vezcolou?rcolor, colour
{n}exactamente n veces\d{4}un año de 4 dígitos
{n,}n o más veces\d{2,}dos o más dígitos
{n,m}entre n y m veces\d{3,5}3 a 5 dígitos
Pattern.matches("ab*c", "abbbc");   // true  — three b's
Pattern.matches("ab+c", "ac");      // false — '+' needs at least one b
Pattern.matches("colou?r", "color");// true  — the 'u' is optional
Pattern.matches("\\d{3,5}", "1234");// true  — four digits is within 3..5

Greedy es el modo predeterminado

Un cuantificador sin modificador es greedy: consume la mayor cantidad de entrada posible y luego retrocede — devolviendo caracteres uno a uno — hasta que el resto del patrón pueda coincidir. Por eso un <.+> ingenuo aplicado a HTML consume mucho más que una sola etiqueta:

String html = "<b>one</b>";
Matcher m = Pattern.compile("<.+>").matcher(html);
m.find();
m.group(); // "<b>one</b>"  — '.+' ate everything, then backed up to the last '>'

El motor primero tomó toda la cadena, no encontró un > al final, y retrocedió hasta que un > coincidió — deteniéndose en el último.

Reluctant: añade ? para tomar lo mínimo posible

Añade ? a cualquier cuantificador (*?, +?, ??, {n,m}?) y se vuelve reluctant (también llamado lazy): coincide con el menor número de repeticiones primero y solo expande cuando es necesario. Eso es lo que generalmente quieres al escanear tokens delimitados:

String html = "<b>one</b>";
Matcher m = Pattern.compile("<.+?>").matcher(html);
m.find();
m.group(); // "<b>"  — stopped at the first '>'

Mismo patrón, un carácter extra, comportamiento opuesto: greedy <.+> devuelve toda la cadena mientras que reluctant <.+?> devuelve solo la primera etiqueta.

Possessive: añade + y nunca devuelve nada

Añade + (*+, ++, ?+, {n,m}+) y el cuantificador se vuelve possessive: toma tanto como puede al igual que uno greedy, pero se niega a retroceder. Si el resto del patrón entonces falla, la coincidencia completa falla — no hay retroceso para rescatarla.

// Possessive '.++' eats the final '>' too and won't return it, so no '>' is left
Pattern.compile("<.++>").matcher("<b>one</b>").find(); // false

¿Por qué renunciar a esa flexibilidad? Velocidad y seguridad. Dado que un cuantificador possessive nunca reconsidera, no puede caer en el retroceso catastrófico — la explosión exponencial que congela un hilo en patrones como (a+)+b con una larga secuencia de a sin b. El possessive a++b responde "sin coincidencia" casi al instante.

ModoSintaxisEstrategia¿Retrocede?
GreedyX*toma lo máximo, luego retrocede si es necesario
ReluctantX*?toma lo mínimo, luego añade si es necesario
PossessiveX*+toma lo máximo y lo conservano

Un ejemplo práctico: los tres modos lado a lado

Este programa ejecuta la misma entrada con cuantificadores greedy, reluctant y possessive, y luego ejercita el rango {n,m} y un cuantificador de grupo. Todo aquí es puro java.util.regex del JDK.

java— editable, runs on the server

Lo que podemos aprender de la ejecución:

  • Greedy <.+> imprimió <b>one</b><i>two</i> — toda la cadena. Consumió todo y luego retrocedió hasta el último >, que es exactamente la razón por la que los patrones greedy coinciden en exceso al cruzar delimitadores.
  • Reluctant <.+?> imprimió <b> con la misma entrada. El simple ? cambió la estrategia de "máximo" a "mínimo", deteniéndose en el primer > — la solución para escanear etiqueta por etiqueta.
  • Possessive <.++> imprimió matches=false. Consumió el > final y se negó a devolverlo, así que el > final en el patrón no tenía nada que coincidir y todo el intento falló — el precio de nunca retroceder.
  • \d{3,5} rechazó 12 (no match, muy pocos dígitos), aceptó 123 y 12345 completos, y en 1234567 solo coincidió con 12345 (len 5) — el límite superior 5 lo restringió aunque hubiera más dígitos disponibles.
  • El patrón de grupo (\w+\s*){2,3} coincidió con alpha beta gamma — tres palabras, su máximo — demostrando que el cuantificador se aplicó al grupo entre paréntesis completo, y a++b devolvió false instantáneamente con una larga secuencia de a sin b, mostrando cómo los cuantificadores possessive evitan el retroceso catastrófico.

¿Qué modo debo usar?

  • Usa reluctant (*?, +?) al hacer coincidir contenido entre delimitadores — comillas, etiquetas, corchetes, bloques delimitados. Se detiene en el primer delimitador de cierre en lugar del último, que es casi siempre lo que quieres.
  • Mantén greedy (el predeterminado) cuando genuinamente quieres la coincidencia más larga posible, o cuando solo hay una coincidencia posible de todos modos y el ?/+ extra sería solo ruido.
  • Usa possessive (*+, ++) como herramienta de rendimiento y seguridad con entradas que no controlas. Dado que nunca retrocede, no puede desencadenar un retroceso catastrófico, pero también fallará coincidencias que un cuantificador greedy habría rescatado — así que úsalo solo donde sabes que el retroceso es innecesario.

Una corrección común en el mundo real: un regex que funciona en entradas pequeñas pero se congela en entradas grandes suele ser un cuantificador greedy dentro de un grupo, como (\w+)+. Hacer que el cuantificador interno sea possessive ((\w++)+ o reestructurar el patrón) elimina la explosión exponencial.

Práctica

Práctica
Con la entrada '<b>one</b>', ¿qué coincide el patrón Java '<.+?>' (un cuantificador reluctant) en la primera llamada a find()?
Con la entrada '<b>one</b>', ¿qué coincide el patrón Java '<.+?>' (un cuantificador reluctant) en la primera llamada a find()?
Was this page helpful?