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ícate

Pattern 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

CONSTRUYE TU CARRERA EN IA Y PROGRAMACIÓN SOFTWARE

Accede a +1000 lecciones y cursos con certificado. Mejora tu portfolio con certificados de superación para tu CV.

Plan mensual

19.00 € /mes

Precio normal mensual: 19 €
47 % DE DESCUENTO

Plan anual

10.00 € /mes

Ahorras 108 € al año
Precio normal anual: 120 €
Aprende Java online

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

Test

Listas

Código

Métodos de la clase String

Código

Streams: reduce()

Test

Polimorfismo

Código

Pattern Matching

Código

Streams: flatMap()

Test

Llamada y sobrecarga de funciones

Puzzle

Métodos referenciados

Test

Métodos de la clase String

Código

Representación de Fecha

Puzzle

Operadores lógicos

Test

Inferencia de tipos con var

Código

Tipos de datos

Código

Estructuras de iteración

Puzzle

Streams: forEach()

Test

Objetos

Puzzle

Funciones lambda

Test

Uso de Scanner

Puzzle

CRUD en Java de modelo Customer sobre un ArrayList

Proyecto

Tipos de variables

Puzzle

Streams: collect()

Puzzle

Operadores aritméticos

Puzzle

Arrays y matrices

Código

Clases y objetos

Código

Interfaz funcional Consumer

Test

Interfaces

Código

Enumeraciones Enums

Código

API java.nio 2

Puzzle

API Optional

Test

Interfaz funcional Function

Test

Encapsulación

Test

Interfaces

Código

Uso de API Optional

Puzzle

Representación de Hora

Test

Herencia básica

Test

Clases y objetos

Código

Interfaz funcional Supplier

Puzzle

HashMap

Puzzle

Sobrecarga de métodos

Test

Polimorfismo de tiempo de ejecución

Puzzle

OOP en Java

Proyecto

Sobrecarga de métodos

Código

Clases sealed

Código

Creación de Streams

Test

Records

Código

Encapsulación

Código

Streams: min max

Puzzle

Métodos avanzados de la clase String

Puzzle

Funciones

Código

Polimorfismo de tiempo de compilación

Test

Reto sintaxis Java

Proyecto

Conjuntos

Código

Estructuras de control

Código

Recursión

Código

Excepciones

Puzzle

Herencia avanzada

Puzzle

Estructuras de selección

Test

Uso de interfaces

Test

Operadores

Código

Variables

Código

HashSet

Test

Objeto Scanner

Test

Streams: filter()

Puzzle

Operaciones de Streams

Puzzle

Interfaz funcional Predicate

Puzzle

Streams: sorted()

Test

Configuración de entorno

Test

CRUD en Java de modelo Customer sobre un HashMap

Proyecto

Uso de variables

Test

Clases

Test

Streams: distinct()

Puzzle

Streams: count()

Test

ArrayList

Test

Datos de referencia

Test

Interfaces funcionales

Puzzle

Métodos básicos de la clase String

Test

Tipos de datos

Código

Clases abstractas

Código

Instalación

Test

Funciones

Código

Excepciones

Código

Estructuras de control

Código

Herencia de clases

Código

La clase Scanner

Código

Generics

Código

Streams: map()

Puzzle

Funciones y encapsulamiento

Test

Streams: match

Test

Gestión de errores y excepciones

Código

Datos primitivos

Puzzle

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

Accede GRATIS a Java y certifícate

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