Mira la lección en vídeo
Accede al vídeo completo de esta lección y a más contenido exclusivo con el Plan Plus.
Desbloquear Plan PlusPattern Matching con instanceof: sintaxis y extracción de variables
El pattern matching en Java cambia la forma de verificar y trabajar con tipos de objetos ya que combina la verificación de tipos y la extracción de datos en una sola operación.
La verificación de tipos con instanceof
tradicionalmente requería dos pasos: primero comprobar el tipo y luego realizar un casting explícito:
// Enfoque tradicional
if (objeto instanceof String) {
String texto = (String) objeto; // Casting manual necesario
System.out.println("Longitud: " + texto.length());
}
Con el pattern matching, estos dos pasos se combinan en una sola operacióne:
// Con pattern matching
if (objeto instanceof String texto) { // Verificación y extracción simultánea
System.out.println("Longitud: " + texto.length());
}
El patrón String texto
realiza simultáneamente:
- La verificación de tipo (
objeto instanceof String
) - La extracción del valor en una variable tipada (
texto
)
La variable extraída (texto
en este caso) tiene un alcance limitado al bloque condicionado por la expresión instanceof
. Esto significa que solo está disponible cuando la condición es verdadera, lo que evita posibles errores por referencias nulas.
Es compatible con los operadores lógicos de cortocircuito:
// La variable 'nombre' solo está disponible en la segunda parte de la expresión AND
if (objeto instanceof String nombre && nombre.length() > 5) {
System.out.println("Nombre largo: " + nombre);
}
// Con OR, la variable solo está disponible en el segundo término
if (!(objeto instanceof Integer valor) || valor > 0) {
// 'valor' solo está disponible si la primera condición es falsa
}
El pattern matching con instanceof
resulta especialmente útil cuando se trabaja con jerarquías de clases complejas:
public void procesarMensaje(Object mensaje) {
if (mensaje instanceof TextoMensaje textoMsg) {
System.out.println("Mensaje de texto: " + textoMsg.getContenido());
} else if (mensaje instanceof ImagenMensaje imgMsg) {
System.out.println("Imagen: " + imgMsg.getUrl() + " (" + imgMsg.getTamaño() + "KB)");
} else if (mensaje instanceof ArchivoMensaje archivoMsg && archivoMsg.getTamaño() < 1024) {
System.out.println("Archivo pequeño: " + archivoMsg.getNombre());
}
}
Los beneficios principales de esta característica incluyen:
- Código más limpio y menos redundante
- Mayor seguridad al eliminar posibles
ClassCastException
- Mejor legibilidad al expresar claramente la intención del código
- Reducción de la verbosidad típica en código Java tradicional
Pattern Matching en expresiones switch: caso tipos y guardas
Las expresiones switch mejoradas expanden las capacidades del switch
tradicional para soportar coincidencias basadas en tipos y condiciones complejas.
Tradicionalmente, el switch
en Java estaba limitado a un conjunto reducido de tipos:
- Tipos primitivos (
char
,byte
,short
,int
) - Wrappers (
Character
,Byte
,Short
,Integer
) - Enumeraciones (
Enum
) String
(desde Java 7)
Con el pattern matching, el switch
sirve para evaluar diferentes tipos de objetos y extraer información:
String describir(Object objeto) {
return switch (objeto) {
case Integer i -> "Número entero: " + i;
case String s -> "Texto de longitud " + s.length();
case List<?> lista -> "Lista con " + lista.size() + " elementos";
case null -> "Valor nulo";
default -> "Otro tipo: " + objeto.getClass().getSimpleName();
};
}
El compilador garantiza la exhaustividad de los casos, asegurando que todas las posibilidades están cubiertas. Esto incluye el manejo explícito del caso null
.
También está la adición de guardas (o predicados) usando la palabra clave when
, que permite especificar condiciones adicionales:
int clasificar(Object valor) {
return switch (valor) {
case Integer i when i > 0 -> 1;
case Integer i when i < 0 -> -1;
case Integer i -> 0; // Debe ser 0
case String s when s.isEmpty() -> 0;
case String s when s.length() < 5 -> s.length();
case String s -> 5; // Valores largos se limitan a 5
default -> -99;
};
}
Las guardas permiten refinar los patrones y crear lógicas más precisas, evaluándose solo después de que el patrón de tipo coincida.
El compilador también verifica problemas de dominación de patrones, donde un caso podría hacer que otro nunca se alcance:
// Error: el caso String nunca se alcanzaría
switch (obj) {
case CharSequence cs -> System.out.println("Secuencia");
case String s -> System.out.println("Texto"); // ¡Inalcanzable!
}
Para resolver este problema, los patrones deben ordenarse del más específico al más general:
switch (obj) {
case String s -> System.out.println("Texto");
case CharSequence cs -> System.out.println("Otra secuencia");
}
Las expresiones switch devuelven un valor, lo que permite asignaciones directas:
double calcularPrecio(Object item) {
return switch (item) {
case Producto p when p.esPerecedero() -> p.getPrecio() * 0.9;
case Producto p -> p.getPrecio();
case Servicio s when s.esExpress() -> s.getTarifa() * 1.5;
case Servicio s -> s.getTarifa();
default -> throw new IllegalArgumentException("Elemento no válido");
};
}
El pattern matching en switch
puede combinarse con records y clases selladas (sealed) para crear un sistema de tipos verificable en tiempo de compilación.
Las ventajas de esta característica incluyen:
- Expresividad mejorada para lógica basada en tipos
- Código más declarativo y menos propenso a errores
- Verificación de exhaustividad garantizada por el compilador
- Flexibilidad para combinar verificaciones de tipo con condiciones adicionales
Record patterns: deconstrucción y patrones anidados
Guarda tu progreso
Inicia sesión para no perder tu progreso y accede a miles de tutoriales, ejercicios prácticos y nuestro asistente de IA.
Más de 25.000 desarrolladores ya confían en CertiDevs
Los record patterns permiten la deconstrucción de objetos de tipo record
para acceder directamente a sus componentes mediante una sintaxis declarativa.
Esta característica permite extraer los componentes de un record en una sola operación:
record Punto(int x, int y) { }
void procesarCoordenada(Object obj) {
if (obj instanceof Punto(int x, int y)) {
// x e y están disponibles como variables locales
System.out.println("Distancia al origen: " + Math.sqrt(x*x + y*y));
}
}
En este ejemplo, el patrón Punto(int x, int y)
realiza tres operaciones simultáneamente:
1. Verifica si obj
es una instancia de Punto
2. Extrae el valor de la coordenada x
3. Extrae el valor de la coordenada y
Los record patterns son muy útiles cuando se integran con las expresiones switch:
String describir(Object forma) {
return switch (forma) {
case Círculo(double radio) ->
"Círculo de radio " + radio;
case Rectángulo(double ancho, double alto) when ancho == alto ->
"Cuadrado de lado " + ancho;
case Rectángulo(double ancho, double alto) ->
"Rectángulo de " + ancho + "×" + alto;
default -> "Forma desconocida";
};
}
También pueden crear patrones anidados, que permiten descomponer estructuras de datos complejas en una sola expresión:
record Dirección(String calle, String ciudad, String país) { }
record Empleado(String nombre, Dirección dirección) { }
record Departamento(String nombre, Empleado responsable) { }
void procesarDepartamento(Object obj) {
if (obj instanceof Departamento(String nombre, Empleado(String respNombre, Dirección(var calle, var ciudad, String país)))) {
System.out.println("Departamento: " + nombre);
System.out.println("Dirigido por: " + respNombre);
System.out.println("Ubicación: " + ciudad + ", " + país);
}
}
Este ejemplo muestra cómo un único patrón puede extraer datos de tres niveles de profundidad, algo que tradicionalmente requeriría múltiples verificaciones y llamadas a métodos.
Para mejorar la legibilidad, se puede utilizar la inferencia de tipos con var
o el patrón de guion bajo (_
) para componentes que no necesitamos:
// Uso de var para inferencia de tipo
if (obj instanceof Punto(var x, var y)) {
// x e y se infieren automáticamente como int
}
// Uso del patrón _ para ignorar componentes
if (obj instanceof Persona(String nombre, _)) {
// Solo nos interesa el nombre, ignoramos el resto
}
Los record patterns también pueden incluir guardas para crear condiciones más específicas:
switch (punto) {
case Punto(int x, int y) when x > 0 && y > 0 -> "Primer cuadrante";
case Punto(int x, int y) when x < 0 && y > 0 -> "Segundo cuadrante";
case Punto(int x, int y) when x < 0 && y < 0 -> "Tercer cuadrante";
case Punto(int x, int y) when x > 0 && y < 0 -> "Cuarto cuadrante";
case Punto(0, 0) -> "Origen";
case Punto(int x, 0) -> "Eje X";
case Punto(0, int y) -> "Eje Y";
}
Los beneficios clave de los record patterns incluyen:
- Sintaxis concisa para acceder a componentes de datos estructurados
- Código más declarativo que expresa claramente la intención
- Reducción del código repetitivo para acceder a propiedades anidadas
- Mayor seguridad al garantizar que todos los tipos coinciden correctamente
- Mejor legibilidad especialmente al trabajar con estructuras anidadas complejas
Integración con sealed classes y aplicaciones prácticas
La combinación de pattern matching con sealed classes (clases selladas) crea un sistema de tipos algebraico que permite modelar dominios con precisión y verificación exhaustiva en tiempo de compilación.
Las clases selladas restringen explícitamente qué clases pueden heredar de ellas mediante la cláusula permits
, creando un conjunto cerrado de subtipos:
public sealed interface Forma
permits Círculo, Rectángulo, Triángulo {
}
public record Círculo(double radio) implements Forma { }
public record Rectángulo(double ancho, double alto) implements Forma { }
public record Triángulo(double base, double altura) implements Forma { }
Cuando se combinan con pattern matching, el compilador puede garantizar la exhaustividad sin necesidad de una cláusula default
:
double calcularÁrea(Forma forma) {
return switch (forma) {
case Círculo(double radio) -> Math.PI * radio * radio;
case Rectángulo(double ancho, double alto) -> ancho * alto;
case Triángulo(double base, double altura) -> (base * altura) / 2;
// No es necesario 'default' - el switch es exhaustivo
};
}
Si se añade una nueva forma a la jerarquía sellada, el compilador marcará automáticamente este método como no exhaustivo, obligando a actualizarlo.
Esta integración es especialmente útil para modelar estados de un sistema:
public sealed interface EstadoPedido
permits Creado, Pagado, Enviado, Entregado, Cancelado { }
public record Creado(LocalDateTime fechaCreación) implements EstadoPedido { }
public record Pagado(LocalDateTime fechaPago, String referenciaPago) implements EstadoPedido { }
public record Enviado(LocalDateTime fechaEnvío, String tracking) implements EstadoPedido { }
public record Entregado(LocalDateTime fechaEntrega, String firmadoPor) implements EstadoPedido { }
public record Cancelado(LocalDateTime fechaCancelación, String motivo) implements EstadoPedido { }
String mostrarEstado(EstadoPedido estado) {
return switch (estado) {
case Creado(var fecha) ->
"Pedido creado el " + fecha.format(DateTimeFormatter.ISO_DATE);
case Pagado(var fechaPago, var referencia) ->
"Pagado el " + fechaPago.format(DateTimeFormatter.ISO_DATE) + " (Ref: " + referencia + ")";
case Enviado(var fecha, var tracking) ->
"En camino desde " + fecha.format(DateTimeFormatter.ISO_DATE) + " (Tracking: " + tracking + ")";
case Entregado(var fecha, var firma) ->
"Entregado el " + fecha.format(DateTimeFormatter.ISO_DATE) + " a " + firma;
case Cancelado(var fecha, var motivo) ->
"Cancelado: " + motivo;
};
}
Otra aplicación práctica es la implementación de parsers o procesadores de documentos:
public sealed interface JsonElement permits JsonObject, JsonArray, JsonPrimitive, JsonNull { }
public record JsonObject(Map<String, JsonElement> properties) implements JsonElement { }
public record JsonArray(List<JsonElement> elements) implements JsonElement { }
public sealed interface JsonPrimitive extends JsonElement
permits JsonString, JsonNumber, JsonBoolean { }
public record JsonString(String value) implements JsonPrimitive { }
public record JsonNumber(double value) implements JsonPrimitive { }
public record JsonBoolean(boolean value) implements JsonPrimitive { }
public enum JsonNull implements JsonElement { INSTANCE }
String convertirJson(JsonElement elemento) {
return switch (elemento) {
case JsonObject(var props) -> {
StringBuilder sb = new StringBuilder("{");
// Procesamiento de propiedades
yield sb.append("}").toString();
}
case JsonArray(var elementos) -> {
StringBuilder sb = new StringBuilder("[");
// Procesamiento de elementos
yield sb.append("]").toString();
}
case JsonString(var valor) -> "\"" + valor + "\"";
case JsonNumber(var valor) -> String.valueOf(valor);
case JsonBoolean(var valor) -> String.valueOf(valor);
case JsonNull.INSTANCE -> "null";
};
}
Un caso de uso muy común es la implementación de árboles de sintaxis abstracta (AST) para lenguajes o expresiones:
public sealed interface Expresión
permits Literal, Variable, Operación { }
public record Literal(double valor) implements Expresión { }
public record Variable(String nombre) implements Expresión { }
public sealed interface Operación extends Expresión
permits Suma, Resta, Multiplicación, División { }
public record Suma(Expresión izquierda, Expresión derecha) implements Operación { }
public record Resta(Expresión izquierda, Expresión derecha) implements Operación { }
public record Multiplicación(Expresión izquierda, Expresión derecha) implements Operación { }
public record División(Expresión izquierda, Expresión derecha) implements Operación { }
double evaluar(Expresión expr, Map<String, Double> variables) {
return switch (expr) {
case Literal(var valor) -> valor;
case Variable(var nombre) ->
variables.getOrDefault(nombre, 0.0);
case Suma(var izq, var der) ->
evaluar(izq, variables) + evaluar(der, variables);
case Resta(var izq, var der) ->
evaluar(izq, variables) - evaluar(der, variables);
case Multiplicación(var izq, var der) ->
evaluar(izq, variables) * evaluar(der, variables);
case División(var izq, var der) ->
evaluar(izq, variables) / evaluar(der, variables);
};
}
Los beneficios principales de esta integración incluyen:
- Modelado de dominio preciso: Las clases selladas definen claramente el conjunto completo de casos
- Verificación de exhaustividad: El compilador garantiza el manejo de todos los casos posibles
- Seguridad en tiempo de compilación: Los errores por casos no manejados se detectan temprano
- Mantenimiento seguro: Al añadir nuevos casos, el compilador señala todos los lugares donde se necesitan cambios
- Implementación de patrones de diseño: Por ejemplo, el Patrón Visitor se puede implementar sin código boilerplate
Aprendizajes de esta lección de Java
- Comprender la sintaxis del pattern matching con instanceof
- Aprender a extraer variables de manera concisa en Java
- Mejorar la legibilidad del código evitando castings innecesarios
- Conocer las reglas y limitaciones del uso de pattern matching
- Aprender a combinar pattern matching con otras estructuras de control
- Entender la integración de pattern matching en expresiones switch y su aplicación con clases selladas
Completa este curso de Java y certifícate
Únete a nuestra plataforma de cursos de programación y accede a miles de tutoriales, ejercicios prácticos, proyectos reales y nuestro asistente de IA personalizado para acelerar tu aprendizaje.
Asistente IA
Resuelve dudas al instante
Ejercicios
Practica con proyectos reales
Certificados
Valida tus conocimientos
Más de 25.000 desarrolladores ya se han certificado con CertiDevs