Programación Orientada a Objetos
Records y clases de datos
El contenedor de valores en una línea: qué escribe el compilador, dónde vive el invariante y cuándo sigue haciendo falta una clase.
Desde Java 17
Este capítulo cubre Java 17. Estás leyendo en .
El contenedor de valores, escrito a mano
El capítulo de encapsulamiento dejó una excepción honesta: un campo público en un contenedor de valores pequeño. La alternativa sin trampa, con los campos privados y el contrato completo, pide cinco piezas para poder usar el objeto como clave de un mapa, y cuatro no dicen nada del problema:
import java.util.Objects; // Java 7
public final class Punto {
private final int x;
private final int y;
public Punto(int x, int y) {
this.x = x;
this.y = y;
}
public int x() { return x; }
public int y() { return y; }
@Override
public boolean equals(Object otro) {
return otro instanceof Punto p && p.x == x && p.y == y;
}
@Override
public int hashCode() {
return Objects.hash(x, y);
}
@Override
public String toString() {
return "Punto[x=" + x + ", y=" + y + "]";
}
}Más de veinte líneas para guardar dos enteros, y la mitad son mecánicas: el compilador las escribe
igual de bien, sin olvidar el hashCode cuando alguien toca el equals. Los records existen para eso.
La cabecera ya es el contrato
Un record declara sus componentes en la cabecera y nada más:
public record Punto(int x, int y) {}
Punto p = new Punto(1, 2);
System.out.println(p.x() + ", " + p.y()); // 1, 2
System.out.println(p); // Punto[x=1, y=2]
System.out.println(p.equals(new Punto(1, 2))); // trueEsa línea es una clase, no un atajo: tiene sus campos, sus métodos y su contrato. Lo que el compilador escribe por ti:
| Pieza | Lo que declara |
|---|---|
| Campos | Uno private final por componente, con su nombre y su tipo |
| Accesores | x() e y(), con el nombre exacto del componente, no getX() |
equals y hashCode | Sobre los componentes, en el orden de la cabecera |
toString | Punto[x=1, y=2], la clase y cada componente |
| La clase | final, y extiende java.lang.Record |
El accesor se llama como el componente porque el componente es la propiedad pública del dato, y
equals y hashCode salen del mismo sitio: dos Punto iguales comparten hash y no se contradicen.
De la clase escrita a mano al record
-
01Antes
Punto.javaCampos, constructor, accesores,equals,hashCodeytoStringa mano. -
02La cabecera
record Punto(int x, int y) {}Los componentes se declaran una vez y el resto lo escribe el compilador. -
03Lo que añades tú
public Punto { ... }El invariante y los métodos que el dato necesita de verdad.
No son un quinto pilar, como avisaba el primer capítulo de la sección: son la forma corta de escribir
una clase de datos, y == sigue preguntando por la identidad. Llegaron en vista previa con Java 14 y
15, son estándar desde Java 16 y esta página se lee desde Java 17, la primera LTS que los incluye.
El constructor compacto y su invariante
El invariante del capítulo 2 no desaparece: cambia de sitio. El constructor canónico recibe todos los componentes en orden, y escribirlo entero no aporta nada, porque es lo que el compilador ya hacía. La forma útil es el constructor compacto: sin lista de parámetros y sin asignaciones, solo la validación.
public record Rango(int min, int max) {
public Rango {
if (min > max) {
throw new IllegalArgumentException("El mínimo no puede superar el máximo");
}
}
}El cuerpo corre antes de que los campos reciban su valor, y dentro min y max son los parámetros.
Como no son final, puedes corregirlos antes de la asignación: intercambiarlos ahí dentro deja el
Rango ordenado. Asignar el campo a mano no se puede: this.min = min no compila, es final.
Un record no tiene constructor sin argumentos: el canónico pide todos los componentes, y si quieres
uno vacío lo escribes tú delegando en él, public Punto() { this(0, 0); }.
Un record como clave de mapa
El capítulo anterior dejó la pregunta a medias: una clave inmutable no cambia de hash dentro del mapa.
Un record la contesta sin escribir nada, porque equals y hashCode salen de los mismos componentes.
Map<Punto, Articulo> catalogo = new HashMap<>();
catalogo.put(new Punto(1, 2), new Articulo("Teclado", 3));
System.out.println(catalogo.get(new Punto(1, 2)).nombre()); // Teclado
System.out.println(catalogo.get(new Punto(2, 1))); // nullLa primera búsqueda usa un Punto que nadie metió en el mapa y aun así lo encuentra, porque el
mapa pregunta por el valor. La segunda no: (2, 1) no es (1, 2). Con el Punto del capítulo de
encapsulamiento, sin esos dos métodos, la primera línea devolvía null.
Lo que un record puede añadir
El record declara los datos; el comportamiento que necesites va dentro, sin pedir permiso:
public record Punto(int x, int y) implements Comparable<Punto> {
public static Punto origen() { return new Punto(0, 0); }
public Punto conX(int nuevo) { return new Punto(nuevo, y); }
@Override
public int compareTo(Punto otro) {
int porX = Integer.compare(x, otro.x);
return porX != 0 ? porX : Integer.compare(y, otro.y);
}
}
System.out.println(Punto.origen()); // Punto[x=0, y=0]
System.out.println(new Punto(1, 2).conX(9)); // Punto[x=9, y=2]Una fábrica estática cubre los constructores alternativos, conX devuelve otro punto porque no hay
setX, y Comparable entra como en cualquier clase. También puedes sobrescribir un accesor o toString.
Lo que un record no puede hacer
Los límites son la otra cara del trato: si el compilador escribe la mitad de la clase, no puedes cambiar las piezas que escribió.
record Malo(int x) {
private int z; // error: field declaration must be static
}
record Peor(int x) extends Base {} // la cabecera no admite extendsUn record solo tiene los campos de sus componentes y los static que declares: no hay sitio para un
contador de instancia ni para una caché interna. Como ya extiende java.lang.Record y es final, no
hereda de otra clase ni se deja heredar, y no hay setters: un cambio es un objeto nuevo.
Cuándo sigue haciendo falta una clase
Un record es una decisión sobre la forma del dato: la cabecera es la API de la clase, con el orden
de los componentes, sus nombres y su constructor. Eso es una ventaja cuando la forma es el dato, y un
problema cuando la forma es un detalle que quieres poder cambiar. Si el objeto cambia con el tiempo, el
record estorba: el Articulo del capítulo 2 vende y repone, y su stock no se reconstruye en cada
venta. Si guarda estado que no cabe en sus componentes, como una conexión abierta, tampoco. En los
tres casos, la clase del capítulo 2 sigue siendo la herramienta.
Clase inmutable o record
| Pregunta | Clase inmutable (capítulo 2) | Record |
|---|---|---|
equals, hashCode, toString | A mano, unas veinte líneas | Los escribe el compilador |
| Accesores | x(), getX() o lo que elijas | x(), el nombre del componente |
| Campos | private final, uno a uno | Los de la cabecera, sin escribirlos |
| Validación | En el constructor | En el constructor compacto |
| Herencia | Puede extender y ser extendida | final, y ya viene de Record |
| Campos extra | Los que necesite | Solo componentes y static |
| Mutabilidad | La que escribas | Superficial: un componente mutable manda |
| Cuándo | Identidad, cambio o estado oculto | El dato es exactamente sus componentes |
Patrones de record (Java 21)
Desde Java 21, un instanceof puede desestructurar el record y darte los componentes ya extraídos,
if (o instanceof Punto(int x, int y)), y lo mismo dentro de un switch; este capítulo no enseña esa
sintaxis, que tiene su sitio en Java 21.
Dónde encajan en el código real
Un record es el tipo del dato que viaja y no se edita: la pareja de mínimo y máximo que devuelve un cálculo, el evento que ya ocurrió y solo se lee, el valor de configuración que se carga al arrancar, la clave de un índice. En el momento en que el objeto tiene que cambiar, guardar algo que no está en sus componentes o esconder su forma, vuelve a ser una clase.
Qué te llevas
- Un record es una clase de datos cuyo contrato entero cabe en la cabecera: campos
private final, accesoresx(),equals,hashCodeytoStringsobre los componentes, clasefinalque extiendejava.lang.Record. - Los records son estándar desde Java 16, con vista previa en 14 y 15, y la página se lee desde Java 17, la primera LTS que los tiene.
- El constructor compacto valida y normaliza sin repetir asignaciones, y los campos los asigna el compilador al final.
- Un record es una clave de mapa fiable porque
equalsyhashCodesalen de los mismos componentes. - La inmutabilidad es superficial: un componente lista o array sigue siendo mutable y rompe la clave.
- Puede tener métodos, fábricas estáticas e interfaces; no puede tener campos de instancia extra, heredar de otra clase ni setters.
- Si el objeto cambia, esconde su forma o guarda estado que no son sus componentes, sigue siendo una clase.