Java
Tutorial Java: Pattern Matching
Java Pattern Matching con instanceof: Optimiza tu código simplificando la comprobación de tipos y extracción de variables. Actualizado a 2023.
Aprende Java y certifícatePattern 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
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
Ejercicios de esta lección Pattern Matching
Evalúa tus conocimientos de esta lección Pattern Matching con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.
Clases abstractas
Listas
Métodos de la clase String
Streams: reduce()
Polimorfismo
Pattern Matching
Streams: flatMap()
Llamada y sobrecarga de funciones
Métodos referenciados
Métodos de la clase String
Representación de Fecha
Operadores lógicos
Inferencia de tipos con var
Tipos de datos
Estructuras de iteración
Streams: forEach()
Objetos
Funciones lambda
Uso de Scanner
CRUD en Java de modelo Customer sobre un ArrayList
Tipos de variables
Streams: collect()
Operadores aritméticos
Arrays y matrices
Clases y objetos
Interfaz funcional Consumer
Interfaces
Enumeraciones Enums
API java.nio 2
API Optional
Interfaz funcional Function
Encapsulación
Interfaces
Uso de API Optional
Representación de Hora
Herencia básica
Clases y objetos
Interfaz funcional Supplier
HashMap
Sobrecarga de métodos
Polimorfismo de tiempo de ejecución
OOP en Java
Sobrecarga de métodos
Clases sealed
Creación de Streams
Records
Encapsulación
Streams: min max
Métodos avanzados de la clase String
Funciones
Polimorfismo de tiempo de compilación
Reto sintaxis Java
Conjuntos
Estructuras de control
Recursión
Excepciones
Herencia avanzada
Estructuras de selección
Uso de interfaces
Operadores
Variables
HashSet
Objeto Scanner
Streams: filter()
Operaciones de Streams
Interfaz funcional Predicate
Streams: sorted()
Configuración de entorno
CRUD en Java de modelo Customer sobre un HashMap
Uso de variables
Clases
Streams: distinct()
Streams: count()
ArrayList
Datos de referencia
Interfaces funcionales
Métodos básicos de la clase String
Tipos de datos
Clases abstractas
Instalación
Funciones
Excepciones
Estructuras de control
Herencia de clases
La clase Scanner
Generics
Streams: map()
Funciones y encapsulamiento
Streams: match
Gestión de errores y excepciones
Datos primitivos
Todas las lecciones de Java
Accede a todas las lecciones de Java y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.
Instalación De Java
Introducción Y Entorno
Configuración De Entorno Java
Introducción Y Entorno
Tipos De Datos
Sintaxis
Variables
Sintaxis
Operadores
Sintaxis
Estructuras De Control
Sintaxis
Funciones
Sintaxis
Recursión
Sintaxis
Excepciones
Programación Orientada A Objetos
Clases Y Objetos
Programación Orientada A Objetos
Encapsulación
Programación Orientada A Objetos
Herencia
Programación Orientada A Objetos
Clases Abstractas
Programación Orientada A Objetos
Interfaces
Programación Orientada A Objetos
Sobrecarga De Métodos
Programación Orientada A Objetos
Polimorfismo
Programación Orientada A Objetos
La Clase Scanner
Programación Orientada A Objetos
Métodos De La Clase String
Programación Orientada A Objetos
Records
Programación Orientada A Objetos
Pattern Matching
Programación Orientada A Objetos
Inferencia De Tipos Con Var
Programación Orientada A Objetos
Enumeraciones Enums
Programación Orientada A Objetos
Generics
Programación Orientada A Objetos
Clases Sealed
Programación Orientada A Objetos
Listas
Framework Collections
Conjuntos
Framework Collections
Mapas
Framework Collections
Funciones Lambda
Programación Funcional
Interfaz Funcional Consumer
Programación Funcional
Interfaz Funcional Predicate
Programación Funcional
Interfaz Funcional Supplier
Programación Funcional
Interfaz Funcional Function
Programación Funcional
Métodos Referenciados
Programación Funcional
Creación De Streams
Programación Funcional
Operaciones Intermedias Con Streams: Map()
Programación Funcional
Operaciones Intermedias Con Streams: Filter()
Programación Funcional
Operaciones Intermedias Con Streams: Distinct()
Programación Funcional
Operaciones Finales Con Streams: Collect()
Programación Funcional
Operaciones Finales Con Streams: Min Max
Programación Funcional
Operaciones Intermedias Con Streams: Flatmap()
Programación Funcional
Operaciones Intermedias Con Streams: Sorted()
Programación Funcional
Operaciones Finales Con Streams: Reduce()
Programación Funcional
Operaciones Finales Con Streams: Foreach()
Programación Funcional
Operaciones Finales Con Streams: Count()
Programación Funcional
Operaciones Finales Con Streams: Match
Programación Funcional
Api Optional
Programación Funcional
Api Java.nio 2
Entrada Y Salida (Io)
Api Java.time
Api Java.time
Ecosistema Jakarta Ee De Java
Frameworks Para Java
Certificados de superación de Java
Supera todos los ejercicios de programación del curso de Java y obtén certificados de superación para mejorar tu currículum y tu empleabilidad.
En esta lección
Objetivos de aprendizaje de esta lección
- 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