CodeGym /Cursos /JAVA 25 SELF /Colecciones inmutables: Collections.unmodifiable

Colecciones inmutables: Collections.unmodifiable

JAVA 25 SELF
Nivel 33 , Lección 2
Disponible

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
Collections.unmodifiableList(list)
No 1.2
List.of(...), Set.of(...), Map.of(...)
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.

Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION