1. El problema de la mutabilidad de las colecciones
En Java, las colecciones recuerdan a un almacén con mercancías: cualquiera puede venir y añadir, quitar o cambiar algo. A veces es cómodo, pero en programas grandes se convierte en un dolor de cabeza. Imagina que has expuesto una lista de productos desde tu clase y alguien borra la mitad de los elementos. O —lo que es aún más divertido— en un programa multihilo un hilo añade elementos mientras otro los lee: el resultado puede ser inesperado y los errores difíciles de atrapar (por ejemplo, ConcurrentModificationException).
He aquí un ejemplo de por qué la mutabilidad de las colecciones es una fuente de bugs:
import java.util.*;
public class Inventory {
private List<String> products = new ArrayList<>();
public Inventory() {
products.add("Té");
products.add("Café");
}
public List<String> getProducts() {
// ¡PELIGRO! Devolvemos una referencia a la lista interna
return products;
}
}
public class Main {
public static void main(String[] args) {
Inventory inv = new Inventory();
List<String> external = inv.getProducts();
external.remove("Té"); // ¡Ups! Ahora ya no hay té en el inventario
System.out.println(inv.getProducts()); // [Café]
}
}
¿Notaste la trampa? Un método devuelve la colección interna y otro la modifica. Así se pueden destruir accidentalmente datos que deberían estar protegidos.
2. Creación de colecciones inmutables: Collections.unmodifiable*
Para evitar estos bochornos, Java ofrece una protección eficaz: puedes hacer que una colección sea «inmutable» usando envolturas especiales de la clase Collections:
- Collections.unmodifiableList(list)
- Collections.unmodifiableSet(set)
- Collections.unmodifiableMap(map)
¿Cómo funciona? Primero creas una colección normal y luego la envuelves en una «envoltura» inmutable:
import java.util.*;
public class Main {
public static void main(String[] args) {
List<String> drinks = new ArrayList<>();
drinks.add("Té");
drinks.add("Café");
List<String> immutableDrinks = Collections.unmodifiableList(drinks);
System.out.println(immutableDrinks); // [Té, Café]
// Intentemos añadir un elemento
immutableDrinks.add("Cacao"); // ¡Pum! UnsupportedOperationException
}
}
Intentar modificar una colección así provoca la excepción UnsupportedOperationException. Es como si pegaras en la caja una enorme etiqueta «¡NO TOCAR!»: cualquiera que intente añadir o eliminar algo recibirá un golpe (o un golpe en la pila de llamadas).
Ejemplo: protegemos el estado interno
Arreglemos nuestra clase Inventory del ejemplo anterior:
import java.util.*;
public class Inventory {
private List<String> products = new ArrayList<>();
public Inventory() {
products.add("Té");
products.add("Café");
}
public List<String> getProducts() {
// Ahora devolvemos una envoltura
return Collections.unmodifiableList(products);
}
}
Ahora, si alguien intenta modificar la lista obtenida, recibirá una excepción.
3. Comportamiento de las colecciones inmutables: protección superficial
Es importante entender: unmodifiableList y sus «hermanos» solo ponen una envoltura alrededor de la colección original. No crean una copia: ¡cualquier cambio en la colección original (la que está «dentro») será visible también en la envoltura!
Demostración
import java.util.*;
public class Main {
public static void main(String[] args) {
List<String> drinks = new ArrayList<>();
drinks.add("Té");
List<String> immutableDrinks = Collections.unmodifiableList(drinks);
drinks.add("Café"); // Modificamos la colección original
System.out.println(immutableDrinks); // [Té, Café] — ¡el elemento apareció!
}
}
Conclusión: la envoltura solo protege contra modificaciones a través de la propia envoltura. Si alguien conserva una referencia a la colección original, podrá seguir modificándola.
4. Inmutabilidad profunda: mitos y realidad
Las envolturas unmodifiable* hacen que la colección sea inmutable solo por fuera. Pero si la colección contiene objetos mutables, ¡se pueden modificar!
Ejemplo
import java.util.*;
class Product {
String name;
Product(String name) {
this.name = name;
}
public String toString() {
return name;
}
}
public class Main {
public static void main(String[] args) {
List<Product> products = new ArrayList<>();
products.add(new Product("Té"));
List<Product> immutableProducts = Collections.unmodifiableList(products);
// Modificamos el objeto dentro de la colección
immutableProducts.get(0).name = "Café";
System.out.println(immutableProducts); // [Café]
}
}
Conclusión:
- La colección es «inmutable», pero los objetos dentro no lo son.
- Para una inmutabilidad completa (profunda) usa objetos inmutables (por ejemplo, String, Integer, clases record o haz tus propias clases inmutables).
5. Cuándo usar colecciones inmutables
Para proteger el estado interno
Si escribes una clase que guarda una colección y la expones hacia fuera, devuelve siempre una envoltura para que nadie pueda modificar tus datos por accidente (o a propósito):
public List<String> getProducts() {
return Collections.unmodifiableList(products);
}
En aplicaciones multihilo
En aplicaciones multihilo las colecciones mutables son una fuente de problemas (condición de carrera, ConcurrentModificationException y otras «alegrías» de la vida). Si no necesitas cambiar una colección después de crearla, hazla inmutable.
Para transferir datos entre capas
Si pasas una colección de una capa del programa a otra (por ejemplo, de DAO a servicio), pasa una copia inmutable o una envoltura: esto protege contra cambios accidentales.
6. Ejemplos prácticos
Ejemplo 1: protegemos la lista de estudiantes
import java.util.*;
public class Group {
private final List<String> students = new ArrayList<>();
public void addStudent(String name) {
students.add(name);
}
public List<String> getStudents() {
return Collections.unmodifiableList(students);
}
}
Nadie podrá añadir o eliminar estudiantes directamente a través de getStudents().
Ejemplo 2: mapa inmutable (Map)
import java.util.*;
public class Main {
public static void main(String[] args) {
Map<String, Integer> grades = new HashMap<>();
grades.put("Bill", 5);
grades.put("Mary", 4);
Map<String, Integer> immutableGrades = Collections.unmodifiableMap(grades);
// immutableGrades.put("Peter", 3); // UnsupportedOperationException
}
}
7. Matices útiles
Alternativas modernas: List.of, Set.of, Map.of
En Java 9 aparecieron formas aún más cómodas de crear colecciones inmutables:
List<String> drinks = List.of("Té", "Café");
Set<String> fruits = Set.of("Manzana", "Plátano");
Map<String, Integer> ages = Map.of("Bill", 20, "Mary", 21);
- Estas colecciones son inmutables (cualquier intento de modificar — excepción).
- No tienen una colección «original» mutable (a diferencia de Collections.unmodifiable*).
- No permiten valores null.
Además, desde Java 10 existen métodos de copia: List.copyOf, Set.copyOf, Map.copyOf — crean una copia inmutable de la colección pasada.
Comparación de formas de crear colecciones inmutables
| Método | Inmutabilidad profunda | ¿Se puede modificar la colección original? | ¿Permite null? | Versión de Java |
|---|---|---|---|---|
|
No | Sí | Sí | 1.2 |
|
No | No (no hay colección original) | No | 9+ |
8. Errores típicos al trabajar con colecciones inmutables
Error n.º 1: Modificar la colección original después de crear la envoltura. Creaste un unmodifiableList, y luego alguien modifica la lista original. La envoltura no te protege de esto: los cambios serán visibles en todos los lugares donde se use la envoltura.
Error n.º 2: Esperar inmutabilidad profunda. Muchos piensan que si una colección es inmutable, entonces los objetos dentro de ella tampoco se pueden modificar. En realidad, solo se protege la estructura (adición/eliminación/modificación a través de la colección), pero no el contenido de los objetos.
Error n.º 3: Usar colecciones inmutables con el valor null en métodos de fábrica modernos. Las colecciones creadas mediante List.of, Set.of, Map.of no permiten null. Intentar añadir u obtener null provocará una excepción.
Error n.º 4: Exponer referencias a colecciones mutables. Si devuelves hacia fuera una referencia a la colección interna (sin envoltura), pierdes el control sobre tus datos: camino directo a bugs y a que se “escapen” los invariantes.
Error n.º 5: Usar colecciones inmutables en código que espera mutabilidad. Si código de terceros intenta modificar la colección (por ejemplo, añadir un elemento), obtendrá UnsupportedOperationException. Asegúrate de que los consumidores sepan que los datos son inmutables.
GO TO FULL VERSION