Records y pattern matching en Java, con ejemplos

Los records son clases de datos en una línea: Java escribe por ti el constructor, los métodos de acceso, equals, hashCode y toString. Combinados con interfaces selladas y el switch con patrones, permiten modelar datos de forma que el compilador avise si se te olvida un caso.

Un record en una línea

record Producto(String nombre, double precio) {}

void main() {
    var cafe = new Producto("Café de Veracruz", 189.50);
    IO.println(cafe);
    IO.println(cafe.nombre() + " cuesta $" + cafe.precio());
    IO.println(cafe.equals(new Producto("Café de Veracruz", 189.50)));
}
salidaProducto[nombre=Café de Veracruz, precio=189.5]
Café de Veracruz cuesta $189.5
true

Los campos son final: un record no cambia después de crearse. Los métodos de acceso se llaman como el campo, sin get: nombre(), no getNombre(). Y dos records con los mismos datos son equals, cosa que con una clase normal habría que programar a mano.

Validar al crear: el constructor compacto

record Correo(String direccion) {
    Correo {
        direccion = direccion.strip().toLowerCase();
        if (!direccion.contains("@")) throw new IllegalArgumentException("correo inválido: " + direccion);
    }
}

void main() {
    IO.println(new Correo("  Ana@Ejemplo.MX "));
    try {
        new Correo("ana.ejemplo.mx");
    } catch (IllegalArgumentException e) {
        IO.println(e.getMessage());
    }
}
salidaCorreo[direccion=ana@ejemplo.mx]
correo inválido: ana.ejemplo.mx

El constructor compacto no repite los parámetros: los recibe ya declarados, puede cambiarlos y validarlos, y Java los asigna a los campos al terminar. Así es imposible que exista un Correo sin arroba.

Interfaces selladas y switch con patrones

sealed interface Pago permits Efectivo, Tarjeta, Transferencia {}
record Efectivo(double monto) implements Pago {}
record Tarjeta(double monto, int meses) implements Pago {}
record Transferencia(double monto, String clabe) implements Pago {}

String describir(Pago p) {
    return switch (p) {
        case Efectivo(double m) -> "efectivo: $%.2f".formatted(m);
        case Tarjeta(double m, int meses) when meses > 1 -> "tarjeta a %d meses: $%.2f al mes".formatted(meses, m / meses);
        case Tarjeta(double m, int meses) -> "tarjeta en una exhibición: $%.2f".formatted(m);
        case Transferencia(double m, String clabe) -> "transferencia desde ...%s: $%.2f".formatted(clabe.substring(14), m);
    };
}

void main() {
    var pagos = java.util.List.of(new Efectivo(250), new Tarjeta(1200, 6), new Tarjeta(80, 1),
            new Transferencia(3400, "012180001234567891"));
    pagos.forEach(p -> IO.println(describir(p)));
}
salidaefectivo: $250.00
tarjeta a 6 meses: $200.00 al mes
tarjeta en una exhibición: $80.00
transferencia desde ...7891: $3400.00

sealed ... permits fija la lista de tipos de pago posibles. Gracias a eso, el switch no necesita default: el compilador sabe que están todos los casos, y si mañana añades un Vales a la interfaz, el switch deja de compilar hasta que lo trates. Los patrones como Tarjeta(double m, int meses) sacan los campos del record directamente, y when añade una condición al caso.

Todo el código se compiló y se ejecutó con Java 25 LTS en un contenedor limpio antes de publicar esta página; las salidas están copiadas de esa ejecución.