Coincidencia de patrones en Java
Usa la coincidencia de patrones en Java para instanceof y switch: patrones de tipo, patrones de registro y deconstrucción.
Durante años, el código Java que trabajaba con valores de tipo desconocido seguía un ritual tedioso: comprobar el tipo con instanceof, luego hacer un cast a ese tipo y después usarlo. La coincidencia de patrones colapsa ese ritual en una sola expresión. Un patrón describe la forma de los datos; si un valor coincide, Java enlaza sus partes a variables que puedes usar de inmediato, sin necesidad de un cast manual.
La coincidencia de patrones llegó por etapas: primero los patrones instanceof, luego los patrones en switch, y después los patrones de registro que deconstruyen registros en sus componentes. En conjunto permiten escribir código declarativo y con seguridad de tipos que se lee como los datos con los que opera.
Este capítulo cubre el patrón instanceof, los patrones de tipo en switch, los patrones con guardas y el manejo de null, y los patrones de registro; luego los une en un programa ejecutable. Se apoya en tres características que quizás quieras revisar antes: el operador instanceof, los registros y las expresiones switch.
Coincidencia de patrones para instanceof
El patrón clásico de prueba y cast necesitaba tres referencias al mismo tipo. El patrón instanceof enlaza una variable al mismo tiempo que realiza la prueba, y el enlace está en alcance en cualquier lugar donde se sabe que la prueba es verdadera.
Object value = "hello";
// Old way: test, then cast
if (value instanceof String) {
String s = (String) value;
System.out.println(s.length());
}
// Pattern way: test and bind together
if (value instanceof String s) {
System.out.println(s.length());
}Como la variable de enlace participa en la expresión booleana, puedes seguir reduciendo en el mismo if. El compilador demuestra que s es seguro de usar:
if (value instanceof String s && s.length() > 3) {
System.out.println(s.toUpperCase());
}Patrones en switch
Un switch puede coincidir con patrones de tipo, despachando según el tipo en tiempo de ejecución del selector. Cada case enlaza el valor coincidente, de modo que el cuerpo trabaja directamente con una variable tipada. Esto convierte largas cadenas if/else instanceof en una tabla compacta y legible.
static String format(Object value) {
return switch (value) {
case Integer i -> "int: " + i;
case Long l -> "long: " + l;
case String s -> "string: " + s;
default -> "other: " + value;
};
}Un switch con patrones de tipo debe ser exhaustivo: tiene que cubrir todas las entradas posibles. Para selectores arbitrarios de tipo Object eso significa una rama default; para jerarquías sealed, el compilador conoce el conjunto completo de subtipos y puede verificar la exhaustividad sin necesidad de un default.
Patrones con guardas y null
Una cláusula when añade una condición booleana a un case, permitiendo que dos valores del mismo tipo tomen ramas distintas. Esto se llama patrón con guarda, y el orden importa: los casos con guarda más específicos van antes del caso de reserva sin guarda.
static String size(String s) {
return switch (s) {
case String t when t.isEmpty() -> "empty";
case String t when t.length() < 5 -> "short";
case String t -> "long (" + t.length() + ")";
};
}Tradicionalmente un switch lanzaba NullPointerException con un selector null. Un switch con patrones puede manejar null explícitamente con un case null, manteniendo la comprobación de nulo dentro del mismo constructo en lugar de una guarda separada antes de él.
| Característica | Sintaxis | Propósito |
|---|---|---|
| Patrón de tipo | case String s | Coincidir por tipo y enlazar |
| Patrón con guarda | case String s when s.isEmpty() | Añadir una condición a un caso |
| Etiqueta null | case null | Coincidir con un selector null |
| Patrón de registro | case Point(int x, int y) | Deconstruir un registro |
Patrones de registro
Un patrón de registro coincide con un registro y enlaza sus componentes en un solo movimiento, evitando las llamadas a los métodos de acceso. Como los registros exponen sus componentes, el compilador conoce la forma exacta y permite nombrar cada parte en línea. Los patrones de registro se anidan, de modo que puedes desestructurar un registro de registros.
record Point(int x, int y) {}
record Line(Point start, Point end) {}
static String render(Object o) {
return switch (o) {
case Point(int x, int y) -> "point " + x + "," + y;
// Nested: pull both endpoints' coordinates out at once
case Line(Point(int x1, int y1), Point(int x2, int y2)) ->
"line " + x1 + "," + y1 + " -> " + x2 + "," + y2;
default -> "unknown";
};
}La coincidencia de patrones brilla con los tipos sellados: cuando una interfaz lista sus implementaciones permitidas, un switch sobre ellas es exhaustivo sin un default, y añadir un nuevo subtipo convierte el caso faltante en un error de compilación en lugar de un error silencioso.
Un ejemplo completo y ejecutable
El programa siguiente une las piezas. Usa un patrón instanceof con una guarda, una jerarquía sellada Shape de registros, patrones de registro que deconstruyen cada forma en un switch, un patrón con guarda que detecta un cuadrado, y un case null, todo sin un solo cast explícito.
Lo que se puede aprender de la ejecución:
describe(42)imprimepositive int 42porque la guardainstanceof Integer i && i > 0prueba el tipo y el valor juntos antes de enlazari.describe(-5)cae enunknown: el mismo patrónIntegercoincide con el tipo pero la guardai > 0falla, mostrando cómo una guarda refina un patrón de tipo.- El switch de
areano necesitadefault:Shapees sellado, por lo que listarCircle,RectangleyTrianglees exhaustivo y el compilador queda satisfecho. - El rectángulo de
5.0 x 5.0se imprime comosquare side=5.0porque su caso guardadowhen w == hestá colocado antes del caso generalRectangle ry gana. - La línea final imprime
no shape: la ramacase nullmaneja un selectornulldentro del switch en lugar de lanzarNullPointerException.