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.
También te puede interesar
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.