1. Comparación entre record y class: ¿cuáles son las principales diferencias?
En Java tenemos dos formas principales de describir nuestros tipos de datos: mediante clases normales (class) y mediante clases record (record). A primera vista, ambas opciones permiten almacenar y procesar datos. Pero si profundizamos un poco, ¡hay más diferencias de las que parece!
Tabla de diferencias: class vs record
| Característica | Clase normal (class) | Clase record (record) |
|---|---|---|
| Mutabilidad | Cualquiera: los campos pueden ser final o no | Inmutable: todos los campos son final |
| Herencia | Puede heredar (extends), no es final por defecto | Siempre final, no puede ser superclase |
| Campos | Cualquiera: estáticos, no estáticos, final o no final, de cualquier tipo | Solo componentes del record (private final), más campos estáticos |
| Getters/setters | Los escribimos nosotros (o los generamos con Lombok) | Se crean getters automáticamente (el nombre del campo es el nombre del método), no hay setters |
| equals/hashCode/toString | Normalmente se escriben a mano/se generan (equals, hashCode, toString) | Se generan automáticamente a partir de todos los componentes |
| Constructores | Cualquiera, tantos como quieras | Uno principal (para todos los componentes); se puede añadir un constructor compacto |
| Interfaces | Se pueden implementar | Se pueden implementar |
| Métodos adicionales | Cualquiera | Se pueden añadir, pero solo métodos (no campos) |
| Uso en colecciones | Se puede, pero hay que implementar correctamente equals/hashCode | Ideal para claves/valores; todo ya está implementado |
Ejemplo ilustrativo
Clase normal:
public class Person {
private final String name;
private final int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
@Override
public boolean equals(Object o) { /* ... */ }
@Override
public int hashCode() { /* ... */ }
@Override
public String toString() { /* ... */ }
}
Clase record:
public record Person(String name, int age) { }
¡Listo! Una sola línea de código — y obtenemos lo mismo (e incluso mejor). Y sin riesgo de olvidar implementar algo importante.
2. Limitaciones de las clases record
Las clases record no son solo «sintaxis corta», sino un concepto aparte con reglas estrictas. Veámoslas con más detalle.
Un record es siempre final
Una clase record por definición es siempre final. Esto significa que no puedes crear una subclase de un record:
public record Point(int x, int y) { }
// public class ColoredPoint extends Point { } // Error de compilación!
Si necesitas ampliar el comportamiento, usa clases normales o composición (incrusta un record dentro de una clase).
Un record no puede ser superclase
Una clase record no puede ser la clase padre de otras clases; siempre es final. Tiene sentido: si eso fuera posible, alguien podría añadir un campo mutable y todo el concepto de «datos inmutables» se vendría abajo.
Solo campos final (componentes)
Todos los componentes de un record se declaran en el encabezado y por defecto son private final. No puedes añadir campos no estáticos en el cuerpo de un record:
public record User(String login, String email) {
// int counter; // Error: no se pueden añadir campos no estáticos
static int totalUsers = 0; // Permitido: es un campo estático
}
No hay setters
Una clase record no puede tener setters para sus componentes. Cualquier intento de añadir un método como setX(int x) no tendrá sentido: no podrás cambiar el valor del campo después de crear el objeto.
public record Point(int x, int y) {
// public void setX(int x) { this.x = x; } // Error: no se puede modificar un campo final
}
No hay constructor vacío
Una clase record siempre tiene un único constructor principal que recibe valores para todos los componentes. No se puede crear un record sin proporcionar todos los datos:
Point p = new Point(1, 2); // OK
// Point p = new Point(); // Error: no existe un constructor sin parámetros
No hay inicializadores no estáticos
Una clase record no puede contener inicializadores no estáticos (los que se escriben entre llaves fuera de los métodos):
public record User(String login) {
// { /* ... */ } // Error: los inicializadores no estáticos están prohibidos
}
Restricciones de herencia
Una clase record no puede heredar explícitamente de otra clase (excepto de java.lang.Record, que está oculta como clase base de todos los record). ¡Pero sí puede implementar interfaces!
public interface Printable {
void print();
}
public record Book(String title) implements Printable {
@Override
public void print() {
System.out.println("Imprimiendo el libro: " + title);
}
}
No es adecuado para lógica de negocio compleja
El record trata de datos, no de comportamiento. Si tu objeto tiene lógica compleja, estado mutable, «ciclo de vida» o muchas dependencias, el record no te ayudará. Es mejor usar una clase normal.
3. ¿Cuándo conviene usar clases record?
- DTO (Data Transfer Object): para transferir datos inmutables entre capas de la aplicación, servicios, microservicios o controladores REST (por ejemplo, en respuestas JSON).
- Value Object: objetos que se definen únicamente por sus valores.
- Claves y valores en colecciones: cuando es importante una implementación correcta de equals y hashCode (por ejemplo, para usar en HashMap o Set).
- Resultados de cálculos: cuando hay que devolver varios valores de un método (por ejemplo, record Pair<T, U>(T first, U second)).
Ejemplo: DTO para un controlador REST
public record UserDto(String login, String email) { }
Ahora puedes devolver con total seguridad un objeto de este tipo desde el controlador, sin temor a que alguien modifique sus campos.
Ejemplo: clave para HashMap
public record Point(int x, int y) { }
Map<Point, String> pointNames = new HashMap<>();
pointNames.put(new Point(1, 2), "A");
pointNames.put(new Point(3, 4), "B");
// ¡Todo funciona correctamente!: equals y hashCode ya están implementados
4. Cuándo NO deberías usar clases record
- Estado mutable: si al menos un campo debe cambiar tras crear el objeto.
- Lógica compleja: si el objeto tiene un comportamiento complejo, muchos métodos u objetos anidados con estado mutable.
- Herencia: si se requiere una jerarquía de clases, clases base abstractas o sobreescritura de métodos.
- Entidades de negocio: por ejemplo, objetos que viven en la base de datos y tienen un identificador único.
Ejemplo: cuando necesitas una clase normal
public class Account {
private String id;
private int balance;
public Account(String id, int balance) {
this.id = id;
this.balance = balance;
}
public void deposit(int amount) { balance += amount; }
public void withdraw(int amount) { balance -= amount; }
// getters, setters, equals, hashCode, toString...
}
Aquí se ve claramente que el estado del objeto cambia — el record no es adecuado.
5. Ejemplos prácticos: elegir entre record y class
Ejemplo 1: record — elección ideal
public record Rectangle(int width, int height) {
public int area() {
return width * height;
}
}
- El rectángulo se define solo por su anchura y altura.
- No es necesario cambiar estos valores tras la creación.
- Puedes añadir un método útil area().
- El resto lo hace Java por ti.
Ejemplo 2: class — mejor opción
public class MutableRectangle {
private int width;
private int height;
public MutableRectangle(int width, int height) {
this.width = width;
this.height = height;
}
public void setWidth(int width) { this.width = width; }
public void setHeight(int height) { this.height = height; }
public int area() { return width * height; }
}
¿Necesitas cambiar las dimensiones del rectángulo después de crearlo? Usa una clase normal.
6. Errores típicos al trabajar con clases record
Error n.º 1: intento de añadir un campo no estático.
Una clase record no permite declarar campos no estáticos fuera de la lista de componentes. Si lo intentas, el compilador dará un error. Por ejemplo:
public record City(String name) {
// int population; // ¡Error!
}
Error n.º 2: querer añadir un setter.
Un record no admite setters para sus componentes. Cualquier intento de cambiar el valor de un campo después de crear el objeto es un error de compilación.
Error n.º 3: intentar heredar de un record o que un record herede.
Un record siempre es final. No se puede heredar de un record y un record no puede heredar de otra clase (salvo del oculto java.lang.Record).
Error n.º 4: usar record para objetos mutables.
Si planeas cambiar el estado del objeto después de crearlo, ¡record no es para ti! Usa una clase normal.
Error n.º 5: olvidar las restricciones del constructor.
Una clase record debe tener obligatoriamente un constructor que reciba valores para todos los componentes. ¡No hay constructor sin parámetros!
GO TO FULL VERSION