Expresiones regulares en Java — teoría con correo, teléfono y DNI (y el error que me hizo pensar que Java estaba roto)
Mi primera expresión regular en Java fue "\d+". Tres caracteres. Sabía perfectamente lo que quería decir: «uno o más dígitos». Y Java me contestó con un rojo furioso:
error: illegal escape character
Pensé que había escrito mal algo tan corto que era imposible. No. El problema es que una expresión regular en Java vive dentro de un String, y el String tiene sus propias reglas para la barra invertida. Eso, junto con un par de ideas más, es lo que hace que las regex parezcan magia negra cuando en realidad son un lenguaje pequeño y muy lógico.
En este artículo vas a aprender a leerlas, escribirlas y usarlas en Java con matches, Pattern, Matcher y grupos, usando un ejemplo que se entiende solo: validar un correo, un teléfono y un DNI.
Tabla de Contenidos
¿Qué es una expresión regular?
Una expresión regular (regex) es un patrón que describe un conjunto de textos. En vez de preguntar «¿es este texto igual a 612345678?», preguntas «¿tiene este texto la forma de un teléfono?».
Sirven para tres cosas principales:
- Validar: ¿este texto cumple el formato? (correo, DNI, contraseña…)
- Buscar y extraer: encontrar fechas, números o correos dentro de un texto largo.
- Reemplazar o trocear: limpiar espacios, cambiar formatos, separar por varios separadores.
Ya usaste regex sin decirlo: split("\\s+") y split("\\.") en el artículo de Strings eran expresiones regulares. Este artículo explica de dónde salían esas barras.
Cómo se escribe una regex en Java: la doble barra
En una regex, \d significa «un dígito». Pero en Java el patrón se escribe dentro de un String, y en un String la barra \ ya tiene significado propio (\n, \t, \"). Java ve \d, no lo reconoce como secuencia de String y se queja con illegal escape character.
La solución: duplicar la barra. \\ en el String se convierte en una sola \ en el patrón.
"12345".matches("\\d+"); // ✅ el patrón real que ve la regex es \d+
"12345".matches("\d+"); // ❌ no compila: illegal escape character
Escribir \d, \s o \. con una sola barra. Regla simple: cada barra de la regex se escribe doble en Java.
Los ladrillos básicos
Caracteres literales
Un carácter normal se representa a sí mismo: "gato".matches("gato") es true.
Clases de caracteres: un carácter de entre varios
| Patrón | Significa | Ejemplo que encaja |
|---|---|---|
[abc] | a, b o c | b |
[a-z] | cualquier minúscula | q |
[A-Za-z] | cualquier letra (sin acentos ni ñ) | Q |
[0-9] | un dígito | 7 |
[^0-9] | cualquier cosa menos un dígito | x |
Atajos
| Atajo | Significa | En Java |
|---|---|---|
\d | un dígito | "\\d" |
\w | letra, dígito o _ | "\\w" |
\s | un espacio, tabulador o salto de línea | "\\s" |
. | cualquier carácter (menos salto de línea) | "." |
Cuantificadores: cuántas veces
| Cuantificador | Significa |
|---|---|
? | 0 o 1 vez |
* | 0 o más veces |
+ | 1 o más veces |
{n} | exactamente n veces |
{n,m} | entre n y m veces |
{n,} | n o más veces |
Un cuantificador se aplica al elemento que tiene justo delante: \\d+ es «uno o más dígitos», y ab+ es una a seguida de una o más b (no «uno o más ab«).
Anclas y alternativas
| Símbolo | Significa |
|---|---|
^ | inicio del texto |
$ | final del texto |
| | «o» (gato|perro) |
( ) | agrupar |
"gato".matches("gato|perro"); // true
"perro".matches("gato|perro"); // true
"gatoperro".matches("gato|perro"); // false: es uno O el otro, no los dos
"gt".matches("ga?to?"); // true: la a y la o son opcionales
Escapar: cuando un símbolo debe ser literal
Los caracteres . * + ? ( ) [ ] { } ^ $ | tienen significado especial. Si quieres el símbolo de verdad, hay que escaparlo con \, es decir, \\ en Java.
"axb".matches("a.b"); // true → el punto es "cualquier carácter"
"axb".matches("a\\.b"); // false → ahora exige un punto de verdad
"a.b".matches("a\\.b"); // true
"1+1".matches("1\\+1"); // true
Si el texto literal es largo y está lleno de símbolos, Pattern.quote("1+1") lo escapa por ti.
Olvidar escapar el punto. "axb".matches("a.b") da true y no te das cuenta hasta que alguien te cuela un axb donde esperabas un a.b. (Es el mismo motivo por el que "2.4.1".split(".") devolvía un array vacío.)
Primera herramienta: matches
String.matches(regex) devuelve true solo si todo el texto encaja con el patrón, de principio a fin.
"12345".matches("\\d+"); // true
"12a45".matches("\\d+"); // false: la 'a' rompe el patrón
"12a45".matches("\\d"); // false: \d es UN dígito, no cinco
Fíjate en que no hace falta poner ^ ni $: matches ya exige que el patrón cubra el texto entero. Eso lo hace perfecto para validar, y poco útil para buscar dentro de un texto largo:
"abc".matches("b"); // false: "b" no es TODO el texto
"abc".matches(".*b.*"); // true: cualquier cosa, una b, cualquier cosa
Validar un teléfono, un DNI y un correo
Vamos paso a paso. Cada patrón, explicado pieza por pieza.
Teléfono español (9 dígitos, empieza por 6, 7, 8 o 9)
String tel = "[6-9]\\d{8}";
// ^^^^^ ^^^^^^
// | └─ 8 dígitos más
// └─ primer dígito: 6, 7, 8 o 9
| Texto | ¿Encaja? | Por qué |
|---|---|---|
612345678 | ✅ | 6 + 8 dígitos |
512345678 | ❌ | empieza por 5 |
61234567 | ❌ | solo 8 dígitos |
6123456789 | ❌ | 10 dígitos: matches exige el texto completo |
61234567a | ❌ | hay una letra |
DNI (8 dígitos y una letra)
String dni = "\\d{8}[A-Za-z]";
Valida el formato, no que el DNI exista. La letra se calcula con el resto de dividir el número entre 23, así que si quieres validarla de verdad, la regex es solo el primer filtro:
String letras = "TRWAGMYFPDXBNJZSQVHLCKE"; int numero = 12345678; char letra = letras.charAt(numero % 23); // 'Z'
Esto es algo que verás mucho en programación real. La regex comprueba la forma, y el código comprueba el significado. Cada herramienta, para lo que sirve.
Correo electrónico
String correo = "[\\w.+-]+@[\\w-]+(\\.[\\w-]+)+"; // ^^^^^^^^^^ ^ ^^^^^^ ^^^^^^^^^^^^^ // | | | └─ uno o más ".algo": .com, .es, .ulpgc.es // | | └─ nombre del dominio // | └─ la arroba // └─ parte local: letras, dígitos, _ . + -
Dentro de [ ], el punto, el + y el guion no hace falta escaparlos: ahí son literales (el guion va al final para que no se interprete como rango).
| Texto | ¿Encaja? |
|---|---|
sergio@sergiolearns.com | ✅ |
sergio.medina@alu.ulpgc.es | ✅ |
sergio@ | ❌ falta el dominio |
@dominio.com | ❌ falta la parte local |
sergio@dominio | ❌ falta el .com o similar |
a b@x.com | ❌ el espacio no está permitido |
No existe una regex perfecta para correos; la especificación real es enorme. Esta es una regex práctica: caza los errores evidentes. Para saber si el correo existe, solo vale enviarle un mensaje.
Segunda herramienta: Pattern y Matcher
matches sirve para validar y se acaba ahí. Cuando necesitas buscar varias veces, extraer partes o reutilizar el patrón, usas dos clases de java.util.regex:
Pattern: el patrón compilado. Se crea una vez.Matcher: el «buscador» que aplica ese patrón a un texto concreto.
import java.util.regex.Pattern;
import java.util.regex.Matcher;
Pattern p = Pattern.compile("\\d+"); // compilamos el patrón una vez
Matcher m = p.matcher("Tengo 3 gatos y 12 peces"); // lo aplicamos a un texto
matches, find y lookingAt: tres preguntas distintas
| Método | Pregunta | Con "\\d+" y el texto… |
|---|---|---|
matches() | ¿todo el texto encaja? | "abc 123 def" → false |
find() | ¿hay alguna parte que encaje? (avanza a la siguiente) | "abc 123 def" → true |
lookingAt() | ¿encaja el principio? | "123abc" → true |
Matcher mm = Pattern.compile("\\d+").matcher("abc 123 def");
System.out.println(mm.matches()); // false
System.out.println(mm.find()); // true (encuentra "123")
System.out.println(mm.group()); // 123
Llamar a group() sin haber hecho antes find() o matches() con resultado true. Lanza IllegalStateException: No match found.
Encontrar todas las coincidencias: while (m.find())
Matcher m = Pattern.compile("\\d+").matcher("Tengo 3 gatos y 12 peces");
while (m.find()) { // cada find() salta a la siguiente coincidencia
System.out.println(m.group() + " en " + m.start() + "-" + m.end());
}
Salida:
3 en 6-7 12 en 16-18
start() y end() dan las posiciones (el final excluido, como en substring). Mismo ejemplo con correos dentro de un texto:
String texto = "Escribe a sergio@sergiolearns.com o a info@x.es hoy";
Matcher m = Pattern.compile("[\\w.+-]+@[\\w-]+(\\.[\\w-]+)+").matcher(texto);
while (m.find()) System.out.println(m.group());
// sergio@sergiolearns.com
// info@x.es
Grupos: sacar partes del texto
Los paréntesis, además de agrupar, capturan esa parte para que puedas leerla después con group(n). El grupo 0 es la coincidencia completa; el 1, lo del primer paréntesis, y así.
Pattern p = Pattern.compile("(\\d{4})-(\\d{2})-(\\d{2})"); // año-mes-día
Matcher m = p.matcher("2026-10-07");
if (m.matches()) {
System.out.println(m.group(0)); // 2026-10-07 (todo)
System.out.println(m.group(1)); // 2026 (año)
System.out.println(m.group(2)); // 10 (mes)
System.out.println(m.group(3)); // 07 (día)
System.out.println(m.groupCount()); // 3 (sin contar el 0)
}
Los grupos se numeran por el orden del paréntesis de apertura. Con muchos grupos se pierde la cuenta, así que existen grupos con nombre:
Matcher m = Pattern.compile("(?<anio>\\d{4})-(?<mes>\\d{2})").matcher("2026-10");
if (m.matches()) System.out.println(m.group("anio") + "/" + m.group("mes")); // 2026/10
En el patrón del correo, (\\.[\\w-]+)+ también es un grupo, pero lo usamos solo para repetir un trozo. Si no necesitas capturarlo, es inofensivo; solo no cuentes con él como group(1) útil.
Reemplazar y trocear con regex
Los métodos de String ya aceptan regex:
"a1b22c333".replaceAll("\\d+", "#"); // "a#b#c#"
"Hola mundo ya".replaceAll("\\s+", " "); // "Hola mundo ya"
"a, b;c d".split("[,;\\s]+"); // [a, b, c, d]
Y en el reemplazo puedes reutilizar los grupos con $1, $2…:
"2026-10-07".replaceAll("(\\d{4})-(\\d{2})-(\\d{2})", "$3/$2/$1"); // "07/10/2026"
Convertimos un formato de fecha en otro sin partir el texto a mano.
Codicioso o perezoso: un detalle que muerde
Los cuantificadores + y * son codiciosos: cogen lo máximo posible. Añadiendo ? detrás se vuelven perezosos y cogen lo mínimo.
String html = "<b>uno</b><b>dos</b>";
// Codicioso: .+ se come todo lo que puede y retrocede hasta el último </b>
Matcher g = Pattern.compile("<b>(.+)</b>").matcher(html);
g.find();
System.out.println(g.group(1)); // uno</b><b>dos
// Perezoso: .+? se para en el primer </b> que encuentra
Matcher l = Pattern.compile("<b>(.+?)</b>").matcher(html);
l.find();
System.out.println(l.group(1)); // uno
Usar .* o .+ esperando «hasta el primer cierre» y llevarse todo hasta el último. Si te pasa, prueba con .*? / .+?.
Opciones (flags) y comprobar contraseñas
Para ignorar mayúsculas, hay dos formas equivalentes:
Pattern.compile("hola", Pattern.CASE_INSENSITIVE).matcher("HOLA").matches(); // true
"HOLA".matches("(?i)hola"); // true
Y un ejemplo con lookahead, (?=...), que significa «aquí a continuación debe haber esto» sin consumirlo. Permite encadenar varias reglas sobre el mismo texto, ideal para contraseñas: al menos una mayúscula, una minúscula, un dígito y 8 caracteres.
String pw = "(?=.*[A-Z])(?=.*[a-z])(?=.*\\d).{8,}";
"Hola1234".matches(pw); // true
"hola1234".matches(pw); // false: falta mayúscula
"Hola12".matches(pw); // false: menos de 8
"HOLAhola".matches(pw); // false: falta dígito
No hace falta dominar los lookahead ahora. Reconócelos cuando los veas: (?=...) es «exige que a continuación haya…».
Cuando la regex está mal escrita
Un patrón inválido no falla al compilar tu programa, sino al ejecutarlo, con PatternSyntaxException:
try {
Pattern.compile("[a-z"); // falta el cierre ]
} catch (PatternSyntaxException e) {
System.out.println(e.getDescription()); // Unclosed character class
}
Olvidar cerrar [ o (. Como es un error de ejecución, solo lo descubres al llegar a esa línea: pruébalo siempre con casos válidos e inválidos.
Resumen y tabla rápida
| Quiero… | Herramienta |
|---|---|
| ¿Todo el texto cumple el formato? | texto.matches(regex) |
| ¿Hay algo que cumpla dentro del texto? | Pattern.compile(...).matcher(texto).find() |
| Todas las coincidencias | while (m.find()) { m.group(); } |
| Sacar partes | paréntesis + m.group(n) o group("nombre") |
| Cambiar texto | replaceAll(regex, reemplazo) con $1, $2… |
| Trocear | split(regex) |
| Ignorar mayúsculas | (?i) o Pattern.CASE_INSENSITIVE |
Tres ideas para quedarte:
- La regex vive en un String: cada
\se escribe\\. matchesexige todo el texto;findbusca dentro.- Los paréntesis agrupan y capturan; los cuantificadores son codiciosos salvo que añadas
?.
En el siguiente artículo lo pondrás en práctica con programas completos que usan estas herramientas.
Siguiente paso: prácticaExpresiones regulares en Java — práctica con un validador, un lector de logs y un limpiador de texto (y el \r invisible que me rompió todas las líneas)¿Te ha servido este artículo? Así puedes apoyar SergioLearns →
