CodeGym /Cursos /JAVA 25 SELF /Colecciones CopyOnWrite, envoltorios inmodificables

Colecciones CopyOnWrite, envoltorios inmodificables

JAVA 25 SELF
Nivel 34, Lección 2
Disponible

1. Envoltorios inmodificables: envoltorios sobre colecciones

A veces en el código ya existe una colección que alguien puede modificar por accidente (o no tan por accidente). Por ejemplo, tienes una lista de usuarios que quieres exponer hacia fuera, pero no quieres que nadie la modifique:

List<String> users = new ArrayList<>();
users.add("Alice");
users.add("Bob");

Devuelves esta lista desde un método, y alguien hace users.add("Hacker"); — ¡y de repente tienes en el sistema un usuario no autorizado! ¿Cómo protegerse?

Envoltorios de Collections

En Java existen desde hace tiempo métodos de envoltorio en la clase Collections:

  • Collections.unmodifiableList(list)
  • Collections.unmodifiableSet(set)
  • Collections.unmodifiableMap(map)

Estos métodos devuelven un envoltorio sobre tu colección, que no permite modificarla a través de él. Un intento de añadir, eliminar o cambiar un elemento mediante el envoltorio lanzará UnsupportedOperationException.

Ejemplo:

import java.util.*;

public class Demo {
    public static void main(String[] args) {
        List<String> modifiable = new ArrayList<>();
        modifiable.add("Alice");
        modifiable.add("Bob");

        // Creamos un envoltorio inmodificable
        List<String> unmodifiable = Collections.unmodifiableList(modifiable);

        System.out.println(unmodifiable); // [Alice, Bob]

        // Intentemos añadir un elemento a través del envoltorio
        try {
            unmodifiable.add("Charlie"); // ¡Pum! UnsupportedOperationException
        } catch (UnsupportedOperationException e) {
            System.out.println("No se puede modificar la colección: " + e);
        }
    }
}

¡Importante!

  • El envoltorio NO convierte la colección original en inmutable. Si alguien conserva una referencia a la colección original, aún puede modificarla.
  • Todos los cambios en la colección original se ven a través del envoltorio.
modifiable.add("Charlie");
System.out.println(unmodifiable); // [Alice, Bob, Charlie]

Es decir, si en alguna parte del código alguien añade/elimina un elemento en la lista original, el envoltorio lo verá. No es una «congelación», sino solo una prohibición de modificar a través del propio envoltorio.

Envoltorios para otras colecciones

De la misma manera se pueden crear envoltorios para Set, Map e incluso para estructuras más exóticas:

Set<Integer> numbers = new HashSet<>(Set.of(1, 2, 3));
Set<Integer> unmodSet = Collections.unmodifiableSet(numbers);

Map<String, Integer> ages = new HashMap<>();
ages.put("Alice", 30);
ages.put("Bob", 25);
Map<String, Integer> unmodMap = Collections.unmodifiableMap(ages);

Comparación con los métodos de fábrica (List.of y otros)

  • List.of(...) crea una colección nueva e inmutable, a la que ni siquiera se puede añadir de inicio.
  • Collections.unmodifiableList(list) — es un envoltorio sobre una colección existente. Si la lista original cambia, también cambiará el envoltorio.

Tabla: comparación de enfoques

List.of(...)
Collections.unmodifiableList(list)
¿Se puede añadir? No No (a través del envoltorio)
¿Se puede añadir en la original? No aplicable Sí
¿Se ven los cambios? No Sí
¿Se puede poner null? No (NPE) Sí (si la colección original lo permite)
Implementación Propia Envoltorio sobre la colección original

2. Colecciones CopyOnWrite

En programas multihilo a menudo surge la tarea: uno (o varios) hilos leen la colección y otro(s) la modifican ocasionalmente. Las colecciones normales no sirven aquí: puede haber condiciones de carrera, errores, ConcurrentModificationException y otras alegrías del mundo concurrente.

Para estos casos se inventaron las colecciones CopyOnWrite: están pensadas específicamente para escenarios en los que las lecturas son frecuentes y las modificaciones, raras.

¿Cómo funciona?

  • Con cada modificación (adición, eliminación, sustitución) la colección crea una nueva copia del array interno.
  • Todos los hilos lectores obtienen «su» versión del array, que no cambia mientras están leyendo.
  • Esto hace que la lectura sea absolutamente segura y no requiere sincronización.

Clases principales

  • CopyOnWriteArrayList<E>
  • CopyOnWriteArraySet<E>

Se encuentran en el paquete java.util.concurrent.

Ejemplo de uso

import java.util.concurrent.CopyOnWriteArrayList;

public class CopyOnWriteDemo {
    public static void main(String[] args) {
        CopyOnWriteArrayList<String> cowList = new CopyOnWriteArrayList<>();
        cowList.add("Alpha");
        cowList.add("Beta");

        // Se puede iterar de forma segura, incluso si alguien añade elementos en paralelo
        for (String s : cowList) {
            System.out.println(s);
            cowList.add("Gamma"); // ¡No lanzará ConcurrentModificationException!
        }

        System.out.println(cowList); // [Alpha, Beta, Gamma, Gamma]
    }
}

Particularidades:

  • El iterador de las colecciones CopyOnWrite siempre «ve» una instantánea de la colección en el momento en que se creó el iterador.
  • Si después de crear el iterador alguien añade elementos, el iterador no los verá.
  • Se pueden añadir/eliminar elementos con seguridad durante la iteración: ¡nada de ConcurrentModificationException!

¿Cuándo usar colecciones CopyOnWrite?

Son adecuadas para situaciones en las que en el programa trabajan muchos hilos que principalmente leen datos de la colección, y las operaciones de modificación se realizan muy raramente. Un ejemplo clásico es la lista de suscriptores de eventos (event listeners): se añaden o eliminan suscriptores con poca frecuencia, pero la notificación de esos suscriptores se ejecuta constantemente.

Ejemplo: suscriptores de eventos

import java.util.concurrent.CopyOnWriteArrayList;

public class EventBus {
    private final CopyOnWriteArrayList<Runnable> listeners = new CopyOnWriteArrayList<>();

    public void subscribe(Runnable listener) {
        listeners.add(listener);
    }

    public void publishEvent() {
        for (Runnable listener : listeners) {
            listener.run(); // seguro, incluso si alguien se suscribe/se da de baja justo ahora
        }
    }
}

Desventajas de las colecciones CopyOnWrite

  • Lento cuando hay cambios frecuentes: cada modificación implica crear una nueva copia del array, lo que es costoso en memoria y tiempo.
  • Ineficiente para colecciones grandes: si la colección es grande, copiar el array es una operación costosa.

3. Comparativa: ¿cuándo usar cada cosa?

Envoltorios inmodificables (Collections.unmodifiable...)

Cuándo usar: Cuando ya tienes una colección que quieres proteger de modificaciones desde código externo, pero los cambios desde dentro (el propietario de la colección) son aceptables.

Seguridad en hilos: ¡No está garantizada! Si la colección original se modifica desde otro hilo, pueden producirse condiciones de carrera y errores.

Métodos de fábrica (List.of, Set.of, Map.of)

Cuándo usar: Cuando quieres crear desde el principio una colección constante e inmutable, sin posibilidad de modificación desde ningún lugar.

Seguridad en hilos: Está garantizada (la colección no cambia en absoluto).

Colecciones CopyOnWrite

Cuándo usar: En escenarios multihilo con muchas lecturas y pocas modificaciones. Por ejemplo, para listas de suscriptores.

Seguridad en hilos: Sí, completamente seguras para hilos.

Inmutabilidad: No; la colección se puede modificar, pero cada vez se crea una nueva copia para que los lectores no se vean afectados.

4. Errores típicos y particularidades de implementación

Error n.º 1: Esperar una «congelación» de la colección original mediante el envoltorio. Muchos piensan que Collections.unmodifiableList(list) hace que la colección sea totalmente inmutable. En realidad, si alguien conserva una referencia a la lista original, puede modificarla, y esos cambios serán visibles a través del envoltorio. Solución: Si necesitas verdadera inmutabilidad, usa List.copyOf(list) (Java 10+) o List.of(...).

Error n.º 2: Usar CopyOnWrite para una colección que cambia con frecuencia. Si en CopyOnWriteArrayList se añaden o eliminan elementos constantemente, esto llevará a problemas de rendimiento y memoria. CopyOnWrite es adecuado solo para escenarios de «muchos lectores, pocos escritores».

Error n.º 3: Esperar que los envoltorios sean seguros para hilos. Collections.unmodifiableList no hace que la colección sea segura para hilos. Si la lista original cambia desde distintos hilos, pueden producirse errores.

Error n.º 4: Usar colecciones de List.of o Set.of con null. A diferencia de las colecciones normales, los métodos de fábrica no admiten null: intentar añadir o incluso crear una colección con null provocará un NullPointerException.

1
Tarea
JAVA 25 SELF, nivel 34, lección 2
Bloqueada
Protección de la lista de palabras prohibidas 🚫
Protección de la lista de palabras prohibidas 🚫
1
Tarea
JAVA 25 SELF, nivel 34, lección 2
Bloqueada
Registro de eventos en tiempo real 📈
Registro de eventos en tiempo real 📈
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION