Record patterns en switch
flowchart TB
Switch[switch x] --> Case1["case Point p,q"]
Switch --> Case2["case Rectangle Point a,b Point c,d"]
Switch --> Case3["case Circle Point c, double r"]
Switch --> Default[default]
Case1 --> Var[Variables ligadas p y q]
Case2 --> Nest[Patrones anidados]
Case3 --> Guard[when r mayor 0]
Switch --> Sealed[Funciona con jerarquías sealed]
Qué son los record patterns
Los record patterns, estabilizados en Java 21, permiten deconstruir un record directamente en un patrón: extraer sus componentes en el mismo sitio donde compruebas su tipo. Son la evolución natural del pattern matching con instanceof (Java 16+) y encajan perfectamente con switch (Java 21+) y sealed interfaces.
La idea: dado record Punto(int x, int y) {}, en lugar de:
// Sin record patterns
if (obj instanceof Punto p) {
int x = p.x();
int y = p.y();
System.out.println("(" + x + ", " + y + ")");
}
Puedes escribir:
// Con record patterns
if (obj instanceof Punto(int x, int y)) {
System.out.println("(" + x + ", " + y + ")");
}
La deconstrucción es type-safe (el compilador verifica que los tipos coinciden con los componentes del record) y exhaustiva (extrae todos los componentes).
En instanceof
Declara dentro de los paréntesis del patrón los componentes que quieres extraer:
record Persona(String nombre, int edad, String email) {}
void imprimir(Object obj) {
if (obj instanceof Persona(String nombre, int edad, String email)) {
System.out.println(nombre + " (" + edad + " años): " + email);
}
}
Cada componente queda como variable local con su tipo correcto.
Con var
Si el tipo es obvio por el record, usa var:
if (obj instanceof Persona(var nombre, var edad, var email)) {
// nombre: String, edad: int, email: String
}
var inferido de los componentes del record declarados.
En switch
Combina record patterns con pattern matching en switch para código totalmente declarativo:
record Circulo(double radio) {}
record Cuadrado(double lado) {}
record Rectangulo(double ancho, double alto) {}
sealed interface Forma permits Circulo, Cuadrado, Rectangulo {}
static double area(Forma forma) {
return switch (forma) {
case Circulo(double r) -> Math.PI * r * r;
case Cuadrado(double l) -> l * l;
case Rectangulo(double a, double h) -> a * h;
};
}
Sin necesidad de accessors intermedios ni variables temporales.
Record patterns anidados
Los records se pueden componer. Los patrones acompañan esa composición:
record Punto(int x, int y) {}
record Rectangulo(Punto topLeft, Punto bottomRight) {}
int ancho(Rectangulo r) {
return switch (r) {
case Rectangulo(Punto(int x1, _), Punto(int x2, _)) -> Math.abs(x2 - x1);
};
}
Puedes anidar tantos niveles como haga falta, extrayendo componentes en cualquier nivel:
record Direccion(String calle, String ciudad, String pais) {}
record Cliente(String nombre, Direccion direccion) {}
record Pedido(Cliente cliente, BigDecimal total) {}
String origen(Pedido pedido) {
return switch (pedido) {
case Pedido(Cliente(var nombre, Direccion(_, var ciudad, var pais)), _) ->
nombre + " desde " + ciudad + ", " + pais;
};
}
El _ (unnamed pattern) se usa para ignorar componentes que no necesitas (disponible en Java 21+ como preview, estable en Java 22+).
Con guardas when
Pattern matching permite añadir condiciones sobre los componentes extraídos:
record Transaccion(String cuenta, BigDecimal cantidad, String tipo) {}
static String procesar(Transaccion t) {
return switch (t) {
case Transaccion(var cta, var cant, var tipo)
when cant.signum() < 0 -> "retirada de " + cta + ": " + cant.abs();
case Transaccion(var cta, var cant, var tipo)
when "INT".equals(tipo) -> "transacción interna: " + cta;
case Transaccion(var cta, var cant, var tipo) -> "normal: " + cta + " " + cant;
};
}
Jerarquías sealed + records
El pattern matching con record patterns sobre una sealed interface es la herramienta definitiva del modelado en Java moderno. El compilador verifica exhaustividad:
sealed interface Arbol<T> permits Hoja, Nodo {}
record Hoja<T>(T valor) implements Arbol<T> {}
record Nodo<T>(Arbol<T> izq, T valor, Arbol<T> der) implements Arbol<T> {}
static <T> int profundidad(Arbol<T> arbol) {
return switch (arbol) {
case Hoja<T> h -> 1;
case Nodo<T>(var izq, var v, var der) ->
1 + Math.max(profundidad(izq), profundidad(der));
};
}
static <T> List<T> inOrder(Arbol<T> arbol) {
return switch (arbol) {
case Hoja<T>(var v) -> List.of(v);
case Nodo<T>(var izq, var v, var der) -> {
var resultado = new ArrayList<T>();
resultado.addAll(inOrder(izq));
resultado.add(v);
resultado.addAll(inOrder(der));
yield List.copyOf(resultado);
}
};
}
El switch está cerrado exhaustivamente por la sealed interface. Añadir un nuevo subtipo forzaría a actualizar aquí (y cualquier otro switch): refactorización dirigida por el compilador.
Ejemplo completo: intérprete de expresiones
Un caso clásico donde record patterns brillan: un pequeño AST (Abstract Syntax Tree).
sealed interface Expr permits Literal, Binaria, Unaria {}
record Literal(double valor) implements Expr {}
record Binaria(String op, Expr izq, Expr der) implements Expr {}
record Unaria(String op, Expr arg) implements Expr {}
static double evaluar(Expr expr) {
return switch (expr) {
case Literal(double v) -> v;
case Binaria("+", var a, var b) -> evaluar(a) + evaluar(b);
case Binaria("-", var a, var b) -> evaluar(a) - evaluar(b);
case Binaria("*", var a, var b) -> evaluar(a) * evaluar(b);
case Binaria("/", var a, var b) -> evaluar(a) / evaluar(b);
case Binaria(var op, _, _) ->
throw new IllegalArgumentException("operador binario desconocido: " + op);
case Unaria("-", var a) -> -evaluar(a);
case Unaria("abs", var a) -> Math.abs(evaluar(a));
case Unaria(var op, _) ->
throw new IllegalArgumentException("operador unario desconocido: " + op);
};
}
// Uso
Expr expr = new Binaria("+",
new Literal(5),
new Binaria("*", new Literal(3), new Literal(4))
);
double resultado = evaluar(expr); // 17.0
En lenguajes como Scala o Haskell, este estilo de programación es natural. Con Java 21 disponemos de la misma expresividad.
Diferencias con accessors manuales
// Manual: accessors repetidos, ruido
if (obj instanceof Pedido p) {
Cliente c = p.cliente();
Direccion d = c.direccion();
String ciudad = d.ciudad();
BigDecimal total = p.total();
// ...
}
// Con record patterns: una línea
if (obj instanceof Pedido(Cliente(_, Direccion(_, var ciudad, _)), var total)) {
// ...
}
Menos ruido, menos variables temporales, menos probabilidad de usar el campo equivocado.
Limitaciones y consideraciones
- Solo funcionan con records, no con clases normales. (Las POJOs tradicionales necesitan
obj instanceof POJO py luego accessors.) - Los tipos de los componentes deben coincidir con los declarados en el record. Si un componente es
Stringy escribescase Persona(int x, ...), el compilador rechaza. - Pattern matching para primitivos llegó después como preview (
case int ien casos específicos), pero para records ya funciona con tipos primitivos en los componentes.
Resumen
Record patterns + pattern matching switch + sealed interfaces componen una tripleta útiles:
- Records definen datos inmutables.
- Sealed cierra jerarquías.
- Pattern matching las consume de forma declarativa, exhaustiva y segura.
Dominar este trío te sitúa en el Java del futuro. Es ya idiomático en proyectos modernos y fundamental para cualquier código de calidad en 2026.
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Java es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de Java
Explora más contenido relacionado con Java y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Deconstruir records en instanceof y switch. Anidar record patterns para datos jerárquicos. Usar var en los componentes para inferencia. Combinar record patterns con guardas when. Aplicar pattern matching sobre jerarquías de sealed. record. Reemplazar accesos tipo rec.campo1().campo2() con deconstrucción limpia.