En Java, los registros (records) combinados con el pattern matching nos ofrecen una forma mucho más limpia y eficiente de trabajar con datos, alejándonos un poco del paradigma clásico orientado a objetos para acercarnos a la programación orientada a datos. Esto nos permite manejar estructuras de datos de manera más directa y expresiva.
Imaginemos que tenemos una jerarquía de tipos geométricos representada mediante una interfaz llamada Geometría y varios records que la implementan, como Rectángulo, Cuadrado y Triángulo. Cada uno de estos records contiene los campos necesarios para describir su forma, por ejemplo, Rectángulo con ancho y alto, Cuadrado con lado, y Triángulo con base y altura.
Para calcular el área de cualquiera de estas figuras, podemos usar pattern matching con la palabra clave instanceof. En versiones modernas de Java, no solo podemos comprobar si un objeto es de un tipo determinado, sino que también podemos extraer directamente sus campos sin necesidad de acceder a ellos mediante getters o variables intermedias. Por ejemplo, en lugar de hacer:
if (g instanceof Rectángulo r) {
float area = r.ancho * r.alto;
}
Podemos hacer destructuring directamente en el patrón:
if (g instanceof Rectángulo(float ancho, float alto)) {
float area = ancho * alto;
}
Esto elimina la necesidad de declarar una variable temporal y acceder a sus campos, haciendo el código más conciso y legible. Además, podemos usar var para que el compilador infiera el tipo de las variables extraídas:
if (g instanceof Rectángulo(var ancho, var alto)) {
float area = ancho * alto;
}
Esta técnica también se puede aplicar dentro de una expresión switch, lo que nos permite manejar múltiples casos de forma elegante y exhaustiva. Por ejemplo:
return switch (g) {
case Rectángulo(float ancho, float alto) -> ancho * alto;
case Cuadrado(float lado) -> lado * lado;
case Triángulo(float base, float altura) -> (base * altura) / 2;
default -> throw new IllegalArgumentException("Geometría desconocida");
};
Sin embargo, para evitar tener que incluir un caso default y garantizar que todos los casos posibles están cubiertos, podemos usar interfaces selladas (sealed interfaces). Si declaramos la interfaz Geometría como sellada y especificamos que solo puede ser implementada por Rectángulo, Cuadrado y Triángulo, el compilador sabe que esos son los únicos casos posibles y no nos obligará a poner un default en el switch.
public sealed interface Geometría permits Rectángulo, Cuadrado, Triángulo {}
Un detalle importante a tener en cuenta es que el orden de los campos en el patrón debe coincidir exactamente con el orden en que están declarados en el record. No podemos extraer solo algunos campos ni cambiar el orden, aunque sí podemos ignorar variables que no necesitemos usando el identificador _, que desde Java 22 es válido para este propósito:
if (g instanceof Rectángulo(var _, var alto)) {
// Solo nos interesa 'alto'
}
Además, el destructuring puede ser anidado. Si un record contiene otro record como campo, podemos extraer directamente los campos internos de forma anidada. Por ejemplo, si tenemos un record Punto con campos x y y, y cada figura tiene un campo centro de tipo Punto, podemos hacer:
if (g instanceof Rectángulo(float ancho, float alto, Punto(var x, var y))) {
// Aquí tenemos acceso directo a ancho, alto, x e y
}
Esto facilita trabajar con estructuras de datos complejas de manera muy limpia y expresiva, algo que es muy común en la programación orientada a datos.
En definitiva, combinar records con pattern matching y sealed interfaces en Java nos permite escribir código más claro, seguro y moderno, aprovechando las características más recientes del lenguaje para manejar jerarquías de datos y sus campos de forma directa y elegante.