1. El problema de las «clases DTO»: ¿por qué necesitamos clases record?
Seamos sinceros: ¿cuántas veces has escrito una clase como esta?
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Point point = (Point) o;
return x == point.x && y == point.y;
}
@Override
public int hashCode() {
return Objects.hash(x, y);
}
@Override
public String toString() {
return "Point{" + "x=" + x + ", y=" + y + '}';
}
}
Parece sencillo, pero ¡mira cuántas líneas! Y ahora imagina que tienes 10 clases así, y en cada una 5–6 campos. Incluso la IDE se cansa de generar ese código repetitivo. Y si decides añadir un campo nuevo, tendrás que tocar el constructor, el getter, equals, hashCode, toString... Aburrido, rutinario y fuente de errores.
A estas clases se las llama DTO (Data Transfer Object) o Value Object. Simplemente almacenan datos — y ya. Pero por culpa del código plantilla resulta pesado mantenerlas.
Si te parece que esto no es un problema, espera a que tengas que modificar 50 de estas clases a la vez. ¡Entonces recordarás las clases record con especial cariño!
2. Introducción a record: sintaxis y la magia de Java 16+
Con Java 16 todo cambió. Apareció un nuevo tipo de clases — record. Están pensadas específicamente para los casos en los que solo hay que almacenar un conjunto de datos. La sintaxis se parece mucho a la de una tupla en otros lenguajes.
¿Cómo declarar un record?
public record Point(int x, int y) { }
¡Y... listo! Acabas de crear una clase inmutable con dos campos, constructor, getters, equals, hashCode y toString. Sin escribir de más.
¿Qué hace Java «bajo el capó»?
- Campos x y y pasan a ser private final.
- Getters se crean automáticamente: int x() y int y().
- Constructor: public Point(int x, int y).
- equals/hashCode: comparan todos los campos por valor.
- toString: devuelve una cadena del tipo "Point[x=1, y=2]".
Se puede decir que un record es «un DTO con esteroides»: menos código, más garantías, menos bugs.
Inmutabilidad (immutability)
Todos los campos de una clase record son automáticamente final. Tras crear el objeto no se puede modificar — lo garantiza el compilador.
Si intentas añadir un setter o hacer que un campo no sea final, el compilador te detendrá. ¡No es frecuente encontrar tanta preocupación por tu tranquilidad!
3. Ejemplo de uso de una clase record
Clase normal (mucho código):
public class Client {
private final String name;
private final int id;
public Client(String name, int id) {
this.name = name;
this.id = id;
}
public String getName() { return name; }
public int getId() { return id; }
// equals, hashCode, toString ...
}
Clase record (¡una sola línea!):
public record Client(String name, int id) { }
Uso:
public class Main {
public static void main(String[] args) {
Client client = new Client("Iván", 123);
System.out.println(client.name()); // Iván
System.out.println(client.id()); // 123
System.out.println(client); // Client[name=Iván, id=123]
}
}
Ten en cuenta:
- Los métodos de acceso a los campos se llaman igual que los campos: name(), id().
- No hay setName() ni setId() — el objeto no se puede modificar tras su creación.
4. Ventajas de las clases record: menos código, menos errores
Menos código — más felicidad
¿Por qué escribir 40 líneas si puedes apañarte con una? Las clases record ahorran tiempo y nervios, especialmente en proyectos grandes con muchos DTO y Value Object.
Inmutabilidad «por contrato»
- Las clases record son siempre final e inmutables.
- No se puede falsificar un objeto ni cambiarlo accidentalmente.
- No hay bugs «raros» por cambios de estado del objeto en lugares inesperados.
- Se pueden usar con seguridad en programas concurrentes (si todos los campos también son inmutables).
Generación automática de equals/hashCode/toString
No hace falta escribir a mano métodos de comparación, cálculo del hash y representación. Todo se genera de forma automática y correcta.
Client c1 = new Client("Ana", 42);
Client c2 = new Client("Ana", 42);
System.out.println(c1.equals(c2)); // true
System.out.println(c1.hashCode() == c2.hashCode()); // true
System.out.println(c1); // Client[name=Ana, id=42]
Ideal para colecciones y claves
Los objetos record pueden usarse como claves en HashMap, elementos en HashSet, etc. — todo funcionará correctamente porque equals y hashCode tienen en cuenta todos los campos.
import java.util.HashMap;
import java.util.Map;
Map<Client, String> clients = new HashMap<>();
clients.put(new Client("Ana", 42), "VIP");
System.out.println(clients.get(new Client("Ana", 42))); // VIP
Descripción explícita de los datos
La sintaxis de una clase record deja claro de inmediato qué datos se almacenan y que el objeto es inmutable. Esto hace que el código sea más comprensible para otros desarrolladores (y para ti dentro de seis meses).
5. Tabla: comparación entre una clase normal y una clase record
| Clase normal | Clase record | |
|---|---|---|
| Sintaxis | Mucho código | Una línea |
| Inmutabilidad | Hay que implementarla explícitamente | Garantizada por el compilador |
| Autogeneración de métodos | No | Sí (equals, hashCode, toString) |
| Se pueden añadir campos | Sí | Solo los componentes del record |
| Herencia | Se puede heredar | Siempre final, no se puede heredar |
| Uso en colecciones | Hay que implementar correctamente los métodos | Funciona de serie |
6. Evolucionando la aplicación educativa: ejemplo con una clase record
Supongamos que en tu aplicación bancaria educativa necesitas guardar operaciones de cuenta: fecha, importe y tipo de operación (por ejemplo, «ingreso» o «retirada»).
Antes de Java 16:
public class Transaction {
private final LocalDate date;
private final double amount;
private final String type;
public Transaction(LocalDate date, double amount, String type) {
this.date = date;
this.amount = amount;
this.type = type;
}
public LocalDate getDate() { return date; }
public double getAmount() { return amount; }
public String getType() { return type; }
// equals, hashCode, toString ...
}
Con Java 16 y record:
import java.time.LocalDate;
public record Transaction(LocalDate date, double amount, String type) { }
Uso:
Transaction t = new Transaction(LocalDate.now(), 100.0, "deposit");
System.out.println(t); // Transaction[date=2024-06-01, amount=100.0, type=deposit]
System.out.println(t.amount()); // 100.0
7. Visualización: qué genera un record
Veamos un record «desplegado» (aproximadamente, lo que generará el compilador):
public final class Point extends java.lang.Record {
private final int x;
private final int y;
public Point(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 o) { /* comparación por campos */ }
@Override
public int hashCode() { /* cálculo por campos */ }
@Override
public String toString() { /* representación legible */ }
}
En breve: cuándo conviene usar record
- Cuando necesitas un objeto inmutable con un conjunto de datos.
- Cuando quieres un equals/hashCode/toString «honestos» sin escribirlos a mano.
- Cuando haces DTO, Value Object, pares, tríos, color, punto, rango, clave para colecciones, etc.
8. Errores típicos al trabajar con clases record
Error №1: intentar añadir un setter o cambiar un campo tras la creación.
Una clase record no permite modificar sus campos. Si intentas añadir un método como setX(int x), el compilador te dirá enseguida «no se puede». Lo mismo ocurre si intentas cambiar un campo directamente.
Error №2: intentar añadir un campo no estático.
En una clase record solo se pueden declarar componentes (los campos entre paréntesis tras el nombre del record) y campos estáticos. No se pueden añadir campos no estáticos — el compilador no te dejará.
Error №3: usar record para lógica mutable.
Las clases record no están pensadas para objetos con estado cambiante. Si necesitas modificar algo después de crear el objeto, usa una clase normal.
Error №4: olvidar que un record es siempre final.
Una clase record no se puede heredar y no puede ser superclase. Intentar saltarse esta limitación provocará un error de compilación. Señal clave: no intentamos «extender» un record; está concebido como un tipo inmutable y cerrado.
Error №5: ignorar los métodos autogenerados.
Si sobrescribes equals, hashCode o toString, ten cuidado — no rompas su contrato, o de lo contrario las colecciones y las comparaciones funcionarán de forma incorrecta.
GO TO FULL VERSION