1. Clases record e inmutabilidad
Inmutabilidad — es la propiedad de un objeto por la cual su estado no puede cambiar después de su creación. Dicho de otro modo: si el objeto ya fue creado, no puedes modificar sus datos internos. ¡Eso es! Como si estuviera grabado en piedra.
Imagina un billete de tren. En cuanto se imprime, no puedes cambiar la fecha ni el lugar de salida (bueno, salvo que hagas trucos con Photoshop). El billete es un objeto inmutable. Si quieres otro billete, compras uno nuevo.
En programación, a este tipo de objetos se les llama objetos inmutables. Protegen el programa de cambios accidentales y hacen el código más seguro y predecible.
Características de un objeto inmutable
- Todos los campos del objeto son final (solo se pueden asignar una vez, normalmente en el constructor).
- No hay setters (métodos que cambian los valores de los campos).
- Todos los métodos que devuelven datos internos o bien devuelven copias, o bien los propios datos también son inmutables.
Las clases record en Java se inventaron precisamente para crear estructuras de datos inmutables de forma sencilla y sin dolor.
¿Por qué un record es inmutable?
- Todos los componentes del record son final.
Cuando declaras un record, el compilador convierte automáticamente todos sus campos en private final. Esto significa que después de crear el objeto no podrás cambiar sus campos. - No hay setters.
En un record no puedes añadir un método setX(int x): el compilador no te permitirá cambiar el valor del campo después de crear el objeto. - El constructor asigna los valores una sola vez.
Todos los valores se establecen únicamente en el momento de crear el objeto.
Ejemplo
public record Point(int x, int y) {}
Point p = new Point(5, 10);
// p.x = 7; // Error de compilación: el campo x tiene acceso private y es final
// p.x(7); // Error: ¡no hay setter!
System.out.println(p.x()); // 5
Un intento de cambiar un campo o de llamar a un setter inexistente provoca un error de compilación. Java vigila estrictamente el contrato de inmutabilidad.
2. Ventajas de los objetos inmutables
Seguridad en entornos multihilo
En programas multihilo (¡que hoy son la mayoría!) los objetos inmutables son como un chaleco antibalas. Si el objeto no puede cambiar, distintos hilos pueden leerlo tranquilamente sin temer que alguien modifique los datos en ese mismo instante. No hace falta sincronizar el acceso ni preocuparse por condiciones de carrera.
Hecho: muchas clases en la biblioteca estándar de Java que se usan intensamente en escenarios multihilo o bien son inmutables, o bien están especialmente protegidas contra cambios.
Facilitan la comprensión del código
Si un objeto es inmutable, siempre tienes la certeza: lo pasas a otro método o clase y no cambiará «a tus espaldas». Esto facilita mucho la lectura y la depuración del código. No hace falta adivinar quién y dónde pudo cambiar un campo: ¡nadie pudo!
Útiles como claves en colecciones
Los objetos inmutables son perfectos para usarlos como claves en colecciones como HashMap o HashSet. ¿Por qué? Porque sus equals y hashCode dependen únicamente de campos que no cambian. Así, el objeto no se «perderá» en la colección debido a un cambio de estado.
Menos errores ocultos
Un objeto mutable es fácil de estropear si pasas su referencia donde no debes. Un objeto inmutable es como un libro impreso: nadie puede arrancar o reescribir una página.
3. Comparación con clases normales
Comparemos el comportamiento de una clase normal y una clase record. Como ejemplo, tomemos un modelo sencillo de un punto en el plano.
Clase normal (mutable)
public class PointClass {
private int x;
private int y;
public PointClass(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public void setX(int x) {
this.x = x;
}
public void setY(int y) {
this.y = y;
}
}
Puedes crear un objeto y luego cambiar su estado cuanto quieras:
PointClass p = new PointClass(1, 2);
p.setX(10); // ahora p.x es 10
Clase record (inmutable)
public record Point(int x, int y) {}
Creas un objeto y listo: será para siempre tal como fue creado:
Point p = new Point(1, 2);
// p.x = 10; // ¡Error! No hay acceso al campo
// p.x(10); // ¡Error! No hay setter
Tabla: comparación de comportamiento
| Clase normal | Clase record | |
|---|---|---|
| Campos | cualesquiera | solo private final |
| Setters | se pueden añadir | no se pueden añadir |
| Mutabilidad | mutable | inmutable |
| Autogeneración | no | sí (equals, hashCode, toString) |
4. Práctica: cómo aprovechar la inmutabilidad de las clases record
Probemos a aplicar una clase record en un ejemplo pequeño. Supón que tenemos una aplicación bancaria y queremos almacenar información sobre una transacción:
public record Transaction(String fromAccount, String toAccount, double amount) {}
Creamos un objeto:
Transaction t = new Transaction("12345", "67890", 1500.0);
System.out.println(t);
// Transaction[fromAccount=12345, toAccount=67890, amount=1500.0]
Intentemos «transferir» el dinero a otra cuenta:
// t.toAccount = "11111"; // ¡Error! El campo es final, no hay acceso
// t.toAccount("11111"); // ¡Error! No hay setter
Si necesitamos otra transacción, creamos un nuevo objeto:
Transaction t2 = new Transaction(t.fromAccount(), "11111", t.amount());
Importante: inmutable no significa «incómodo». Es simplemente otro estilo de trabajo: si necesitas un nuevo estado, creas un nuevo objeto.
5. Particularidades de la inmutabilidad: qué hay que tener en cuenta
¡La inmutabilidad no siempre es absoluta!
Un record garantiza que sus campos no cambiarán. Pero si un campo es una referencia a un objeto mutable (por ejemplo, un array o una clase normal), el contenido de ese objeto sí puede cambiar.
Ejemplo con array
public record DataHolder(int[] data) {}
int[] arr = {1, 2, 3};
DataHolder holder = new DataHolder(arr);
arr[0] = 99;
System.out.println(holder.data()[0]); // 99! ¡El array ha cambiado!
Conclusión: si quieres una inmutabilidad real, usa solo tipos inmutables (String, Integer, otros records, etc.) o haz copias de los objetos mutables dentro del constructor canónico del record. Por ejemplo:
int[] copy = Arrays.copyOf(data, data.length);
6. Cómo hacer tú mismo que una clase normal sea inmutable
Si quieres convertir una clase normal en immutable (inmutable), tendrás que hacerlo a mano:
- Marcar todos los campos como private final,
- No añadir setters,
- Inicializar todos los campos solo a través del constructor,
- Si un campo es un objeto mutable, hacer una copia defensiva (defensive copy).
Ejemplo
public final class User {
private final String name;
private final int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String name() {
return name;
}
public int age() {
return age;
}
}
Estarás de acuerdo en que con un record esto se hace más fácil y más corto:
public record User(String name, int age) {}
7. Errores típicos al trabajar con clases record inmutables
Error n.º 1: intentar cambiar un campo después de la creación.
Los principiantes a menudo intentan escribir p.x = 42; o p.x(42); para un objeto record. Pero el compilador dirá de inmediato: «¡Así no! El campo es final, no hay setter».
Error n.º 2: usar objetos mutables como componentes del record.
Si añadiste al record un campo de tipo List, Map, array u otro objeto mutable, el propio record no te protegerá de cambios en el contenido de ese objeto. Por ejemplo, si tienes un record User(List<String> hobbies), alguien podría añadir o eliminar un elemento de la lista, y eso cambiaría el estado de tu objeto record. Para evitarlo, usa colecciones inmutables (List.copyOf, Collections.unmodifiableList) o haz copias de las colecciones dentro del constructor del record.
Error n.º 3: entender mal la inmutabilidad.
Algunos piensan que si un objeto es un record, está protegido de cualquier cambio. En realidad, si los campos son referencias a objetos mutables, su contenido puede cambiar, lo que provocará errores inesperados.
GO TO FULL VERSION