Clases Pattern y Matcher en Java
Compila patrones y aplica coincidencias en Java con las clases Pattern y Matcher.
Las expresiones regulares en Java se encuentran en el paquete java.util.regex, y casi todo ese paquete se reduce a dos clases: Pattern y Matcher. Un Pattern es una expresión regular compilada — la regla. Un Matcher es el motor que ejecuta esa regla contra un fragmento de entrada y reporta lo que encontró. Separar ambos permite compilar una expresión una sola vez y reutilizarla en miles de entradas, lo que marca la diferencia entre código regex rápido y código regex lento.
Este capítulo cubre la compilación de un Pattern, el uso de un Matcher, la crucial distinción entre find() y matches(), grupos de captura y grupos con nombre, y los flags. Si eres nuevo en la sintaxis en sí, comienza primero con los capítulos de introducción a regex y sintaxis de regex.
Pattern: compilar una vez, reutilizar siempre
Pattern.compile(String regex) analiza la sintaxis de la expresión regular y devuelve un Pattern inmutable y seguro para hilos. La compilación es el paso costoso, así que hazlo una vez — típicamente en un campo static final — y comparte el resultado. Los métodos de conveniencia en String (matches, replaceAll, split) recompilan el patrón en cada llamada, lo que está bien para un uso ocasional pero es un desperdicio en un bucle.
// Good: compile once, reuse
private static final Pattern EMAIL =
Pattern.compile("[\\w.+-]+@[\\w.-]+\\.[a-z]{2,}");
// Wasteful in a loop: String.matches recompiles every iteration
for (String s : lines) {
if (s.matches("[\\w.+-]+@[\\w.-]+\\.[a-z]{2,}")) { /* ... */ }
}Nota las barras invertidas dobles: \\w en el código fuente Java es el token regex \w, porque la barra invertida debe primero sobrevivir el propio escapado de cadenas de Java.
Matcher: el motor con estado
Un Pattern no contiene entrada ni posición — es solo la regla. Llamar a pattern.matcher(input) produce un Matcher vinculado a esa entrada, y el Matcher mantiene todo el estado mutable: la posición de búsqueda actual, los límites de la última coincidencia y los grupos capturados. Por ser con estado, un Matcher no es seguro para hilos; dale a cada hilo el suyo propio.
| Método | Qué hace |
|---|---|
matches() | Comprueba si toda la entrada coincide con el patrón |
lookingAt() | Comprueba si la entrada coincide desde el principio (no necesita llegar al final) |
find() | Encuentra la siguiente coincidencia en cualquier parte de la entrada; devuelve true y avanza |
group() / group(n) | Devuelve la coincidencia completa, o el grupo de captura n |
start() / end() | Índice del primer carácter de la coincidencia y uno más allá del último |
replaceAll(repl) | Reemplaza cada coincidencia, con referencias inversas $1, $2 a grupos |
reset() | Rebobina el matcher a la posición cero (opcionalmente con nueva entrada) |
find() versus matches(): la confusión más común
matches() está anclado a la cadena completa — devuelve true solo si el patrón consume toda la entrada. find() es un escáner: busca el patrón en cualquier parte y puede llamarse repetidamente para recorrer cada ocurrencia.
Pattern p = Pattern.compile("\\d+");
System.out.println(p.matcher("abc123").matches()); // false — whole string isn't digits
System.out.println(p.matcher("abc123").find()); // true — found "123" inside
Matcher m = p.matcher("a1 b22 c333");
while (m.find()) {
System.out.println(m.group() + " @ " + m.start()); // 1@1, 22@4, 333@8
}Un error frecuente es llamar a group() antes de un matches()/find() exitoso — eso lanza IllegalStateException, porque todavía no hay ninguna coincidencia que leer.
Grupos de captura, grupos con nombre y reemplazo
Los paréntesis en una expresión regular crean grupos de captura, numerados de izquierda a derecha comenzando en 1 (el grupo 0 es la coincidencia completa). Java también admite grupos con nombre con (?<name>...), que se leen con group("name") — mucho más legible que contar paréntesis. En las cadenas de reemplazo, $1 y ${name} insertan lo que capturó un grupo. El capítulo de grupos regex profundiza en los grupos no capturadores y las referencias inversas.
Pattern date = Pattern.compile("(?<year>\\d{4})-(?<month>\\d{2})-(?<day>\\d{2})");
Matcher m = date.matcher("2024-11-30");
if (m.matches()) {
System.out.println(m.group("year")); // 2024
System.out.println(m.group(3)); // 30 (third numbered group)
}
// Reformat using back-references
System.out.println("2024-11-30".replaceFirst(
"(\\d{4})-(\\d{2})-(\\d{2})", "$3/$2/$1")); // 30/11/2024Flags: insensible a mayúsculas, multilínea y más
Pasa los flags como segundo argumento a Pattern.compile, combinados con |. Los más usados son CASE_INSENSITIVE, MULTILINE (para que ^/$ coincidan en los saltos de línea) y DOTALL (para que . también coincida con saltos de línea). Los mismos flags pueden establecerse en línea con (?i), (?m), (?s). Consulta el capítulo de flags de regex para la lista completa y sus ventajas e inconvenientes.
Pattern p = Pattern.compile("error", Pattern.CASE_INSENSITIVE);
System.out.println(p.matcher("FATAL ERROR").find()); // true
// Equivalent inline form:
Pattern.compile("(?i)error");Un ejemplo práctico: analizando una línea de log con un patrón compilado
Este programa compila un patrón de fecha una sola vez y lleva a un Matcher a través de sus posibilidades — escaneando cada fecha en una cadena con find(), contrastando find() con matches(), reformateando mediante referencias inversas de grupos, dividiendo en espacios en blanco y leyendo valores de grupos con nombre.
Qué aprender de la ejecución:
find()es un escáner con memoria. El buclewhile (m.find())localizó ambas fechas — en el índice6y en el índice23— porque cada llamada reanuda desde donde terminó la coincidencia anterior. Así es como enumeras cada ocurrencia, y por eso el total resultó ser2.matches()es todo o nada.matches whole log?imprimiófalseporque las fechas están entre otro texto, mientras quematches one date?imprimiótrueporque"2024-01-15"es la entrada completa. Usafind()para localizar,matches()para validar.- Los grupos numerados son accesibles durante la coincidencia. Dentro del bucle,
m.group(1)devolvió solo el año de cuatro dígitos (2024) mientras quem.group()devolvió la fecha completa — el grupo0es la coincidencia, los grupos1..nson las capturas entre paréntesis de izquierda a derecha. - Las referencias inversas reorganizan el texto.
replaceAll("$3/$2/$1")convirtió cadaYYYY-MM-DDenDD/MM/YYYY, produciendostart 15/01/2024 build 30/11/2024 done— el motor sustituyó cada grupo capturado en la plantilla de reemplazo. - Los grupos con nombre se leen como campos. Dividir
"a b c"con\s+colapsó las series de espacios en3partes, y el patrón con nombre permitió quenm.group("user")ynm.group("host")extrajeranaliceyw3docs.compor nombre en lugar de por números de posición frágiles.