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.

¿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:

  1. Validar: ¿este texto cumple el formato? (correo, DNI, contraseña…)
  2. Buscar y extraer: encontrar fechas, números o correos dentro de un texto largo.
  3. 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ónSignificaEjemplo que encaja
[abc]a, b o cb
[a-z]cualquier minúsculaq
[A-Za-z]cualquier letra (sin acentos ni ñ)Q
[0-9]un dígito7
[^0-9]cualquier cosa menos un dígitox

Atajos

AtajoSignificaEn Java
\dun dígito"\\d"
\wletra, dígito o _"\\w"
\sun espacio, tabulador o salto de línea"\\s"
.cualquier carácter (menos salto de línea)"."

Cuantificadores: cuántas veces

CuantificadorSignifica
?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ímboloSignifica
^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étodoPreguntaCon "\\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 coincidenciaswhile (m.find()) { m.group(); }
Sacar partesparéntesis + m.group(n) o group("nombre")
Cambiar textoreplaceAll(regex, reemplazo) con $1, $2…
Trocearsplit(regex)
Ignorar mayúsculas(?i) o Pattern.CASE_INSENSITIVE

Tres ideas para quedarte:

  1. La regex vive en un String: cada \ se escribe \\.
  2. matches exige todo el texto; find busca dentro.
  3. 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 →

Sigue aprendiendo

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *