W3docs

Colecciones no modificables en Java

Crea colecciones inmutables en Java con List.of, Set.of, Map.of y los wrappers Collections.unmodifiable*.

Una colección modificable permite que cualquiera con una referencia cambie su contenido. Una colección no modificable no lo permite: llamar a add, remove, put, clear o set lanza UnsupportedOperationException. Java tiene dos formas complementarias de construir una: las factorías .of(...) introducidas en Java 9 (List.of, Set.of, Map.of, Map.ofEntries) y los antiguos wrappers Collections.unmodifiable*. Se parecen en el punto de llamada, pero se comportan de manera diferente en dos aspectos importantes, y la herramienta correcta depende de lo que realmente necesitas.

Este capítulo cierra la parte del framework de colecciones dándote una receta moderna y clara para "dame una constante" y "dame una instantánea".

Por qué la inmutabilidad

Tres ventajas concretas que justifican el patrón:

  1. Compartición segura. Entrega una lista no modificable a un constructor, un hilo de trabajo o un consumidor de eventos y no tienes que preocuparte de que muten tu estado. El tipo a nivel de compilador no dice "solo lectura", pero el runtime sí.
  2. Hashable de forma segura. Poner una List mutable en un HashSet es un error: si el contenido de la lista cambia, su hashCode cambia y el conjunto pierde el elemento. Las colecciones no modificables evitan esto por completo.
  3. Mejor diseño de API. Devolver una vista no modificable desde un getter dice "esto es mío, léelo, no lo cambies". Sin eso, cada caller tiene que decidir si hacer una copia defensiva.

Las dos estrategias

List.of, Set.of, Map.of, Map.ofEntries — colecciones verdaderamente inmutables

Añadidas en Java 9. Construyen una nueva colección con su propio almacenamiento interno. Nada más tiene una referencia a ella:

List<String> roles  = List.of("admin", "editor", "viewer");
Set<Integer> primes = Set.of(2, 3, 5, 7, 11);
Map<String, Integer> ages = Map.of("alice", 30, "bob", 25);

Map<String, Integer> many = Map.ofEntries(
    Map.entry("alice", 30),
    Map.entry("bob",   25),
    Map.entry("carol", 28)
);

Úsalas para constantes y literales — colecciones pequeñas y fijas que escribes en el código. El JIT las compila a representaciones muy compactas y de bajo coste (a menudo un único array en línea). El coste es cero por asignación más allá de lo que el literal en sí ocupa.

Tres restricciones a recordar:

  1. No hay elementos null, no hay claves null, no hay valores null. List.of("a", null) lanza NullPointerException en la construcción. Si necesitas representar "ausente", usa Optional u omite la clave del mapa.
  2. No hay duplicados para Set.of y Map.of. Set.of("a", "a") lanza IllegalArgumentException. Están diseñadas para datos literales que tú controlas.
  3. Map.of tiene sobrecargas solo hasta 10 entradas. Para 11 o más, usa Map.ofEntries(Map.entry(...), Map.entry(...), ...).

Collections.unmodifiableList(coll) etc. — vistas de una colección existente

Envuelve una colección en una vista de solo lectura. El original sigue siendo mutable, y los cambios a través del original son visibles a través de la vista:

List<String> mutable = new ArrayList<>(List.of("a", "b", "c"));
List<String> view    = Collections.unmodifiableList(mutable);

view.add("d");                      // throws UnsupportedOperationException
mutable.add("d");                   // legal — and the view sees the change
System.out.println(view);            // [a, b, c, d]

Úsalas cuando quieras exponer una colección interna sin copiarla y sin dar permiso a los callers para mutarla. El patrón clásico es un getter:

public List<String> getNames() {
  return Collections.unmodifiableList(this.names);
}

El caller no puede cambiar this.names a través de la vista devuelta. sí puedes. Si también quieres prohibírtelo a ti mismo, copia:

return List.copyOf(this.names);

…que es la tercera estrategia.

List.copyOf, Set.copyOf, Map.copyOf — instantánea y luego congela

Un atajo para "copiar el contenido actual en una nueva colección inmutable":

List<String> snapshot = List.copyOf(mutable);

Después de esta llamada, snapshot es completamente independiente de mutable. Los cambios posteriores a mutable son invisibles a través de snapshot. También existe una optimización inteligente: si la fuente ya es una colección no modificable producida por List.of / List.copyOf, la llamada devuelve la fuente misma, sin ninguna asignación.

copyOf rechaza elementos null, igual que of. Si tu fuente puede contener null, usa Collections.unmodifiableList(new ArrayList<>(source)) en su lugar.

Los tres patrones en resumen

Patrón¿Independiente de la fuente?¿Permite null?Úsalo cuando
List.of("a", "b")n/a (sin fuente)NoConstantes literales
List.copyOf(source)Sí — almacenamiento propioNoInstantánea en un momento concreto
Collections.unmodifiableList(source)No — vistaExponer estado interno de solo lectura

Cuando el punto de llamada dice "¿son estos datos literalmente codificados en duro?", usa of. Cuando dice "quiero una instantánea congelada de lo que hay ahora mismo", usa copyOf. Cuando dice "quiero que el contenido actual sea observable pero no modificable a través de esta referencia", usa unmodifiableList.

Superficial, no profundo

Las tres estrategias son superficiales — congelan la estructura de la colección, no los elementos que contiene.

List<int[]> arrays = List.of(new int[]{1, 2}, new int[]{3, 4});
arrays.add(new int[]{5});                 // UnsupportedOperationException
arrays.get(0)[0] = 99;                    // OK — and now the list contains {99, 2}

Si quieres inmutabilidad profunda, necesitas elegir tipos de elementos que sean a su vez inmutables. Los records con campos primitivos o de tipo String lo son. Los records con campos mutables no lo son. Esta es la misma advertencia que se aplica a las referencias final en general: el vínculo es fijo, el destino puede que no lo sea.

Set.of y Map.of tienen un orden de iteración no especificado

Dos decisiones de diseño intencionales que sorprenden a la gente:

  1. Set.of y Map.of aleatorizan deliberadamente el orden de iteración entre ejecuciones de la misma JVM. Si escribes código que depende de un orden específico de estas, verás pruebas inestables. Usa List.of (que conserva el orden del literal) o un LinkedHashSet/LinkedHashMap envuelto con Collections.unmodifiable* cuando realmente necesites orden.
  2. Set.of(a, b) y Set.of(b, a) pueden iterar de forma diferente incluso en la misma ejecución si los valores tienen distinto hash. No compares por toString.

Esto es por diseño — Java te impide depender accidentalmente del orden para que la implementación sea libre de cambiarlo.

Lo que la no modificabilidad no te da

  • No es segura para hilos en lecturas de campos de elementos mutables. Si los elementos son mutables y otro hilo los está cambiando, necesitas sincronización de todos modos.
  • No hace que la colección subyacente sea thread-safe. Collections.unmodifiableList(arrayList) es una vista de una lista no thread-safe; si otro hilo hace add sobre arrayList, la lectura a través de la vista puede ver un estado corrupto. Para inmutabilidad thread-safe, List.copyOf (o List.of) es la herramienta correcta — tienen almacenamiento privado propio.
  • No hace que .equals sea independiente del orden. Una List devuelta por List.of sigue siendo igual-por-posición respecto a otras listas, no por contenido.

Un ejemplo práctico: literales, instantáneas, vistas y la trampa de la superficialidad

El programa siguiente muestra las tres estrategias lado a lado, demuestra la sorpresa de "la vista ve las mutaciones", la promesa de "la copia es independiente", y la trampa de la superficialidad que atrapa a todos la primera vez.

java— editable, runs on the server

Lo que se puede extraer de la ejecución:

  • List.of y List.copyOf producen ambas una colección verdaderamente inmutable — rechazan toda mutación. Solo se diferencian en si suministraste los datos de forma literal o los copiaste de otro lugar.
  • La vista Collections.unmodifiableList rechazó view.add pero aceptó backing.add a través de la referencia original. Los cambios a través de la lista de respaldo se hicieron visibles a través de la vista. Esa es la característica definitoria de una vista, y la razón por la que esta estrategia no es un sustituto de copyOf en código no confiable.
  • La trampa de la superficialidad es real: los elementos int[] de una List<int[]> inmutable son a su vez mutables, y editar uno reescribe la lista "congelada". Si quieres inmutabilidad profunda, tus elementos ya deben ser inmutables.
  • Set.of rechazó el duplicado y Map.of rechazó el valor null — ambos en la construcción. Estas colecciones fallan rápido y con ruido; eso es una característica.
  • List.copyOf de una lista ya inmutable devolvió la misma instancia sin asignar memoria. Esa es la optimización del JDK, y es la razón por la que "siempre copia a la salida" es barato cuando la fuente ya es inmutable.

Qué viene después — y hacia la Parte 12

Esto cierra la parte del Collections Framework. Ahora conoces cada implementación (ArrayList, LinkedList, HashMap, TreeMap, las colas, los deques y el resto), cada interfaz (Collection, List, Set, Map, Queue, Deque), los cursores de iteración (Iterator, ListIterator), las interfaces de ordenación (Comparable, Comparator), la caja de herramientas estática (Collections) y la historia de la inmutabilidad.

La siguiente parte — Programación Funcional — cambia de marcha. En lugar de cómo almacenar datos, trata cómo expresar transformaciones sobre datos. El primer capítulo, Functional Programming in Java, introduce el modelo mental: funciones como valores, inmutabilidad, funciones puras y composición. A partir de ahí, la parte desarrolla lambdas, referencias a métodos, las interfaces funcionales integradas (Function, Predicate, Consumer, Supplier), Optional y los streams, que usan las colecciones que acabas de aprender como fuente y destino.

La mayoría de los patrones en esta parte — list.sort(Comparator.comparing(Person::name)), map.getOrDefault(k, 0), stream().filter(...).toList() — ya tienen sabor funcional. La Parte 12 hace explícito ese sabor y te muestra cómo usarlo para todo lo demás.

Práctica

Práctica
Tienes un campo `private List<String> names = new ArrayList<>()` y quieres un getter que permita a los callers *leer* el contenido actual pero nunca mutarlo, y que también refleje adiciones posteriores a `names` realizadas por la clase propietaria. ¿Qué expresión de retorno encaja?
Tienes un campo `private List<String> names = new ArrayList<>()` y quieres un getter que permita a los callers *leer* el contenido actual pero nunca mutarlo, y que también refleje adiciones posteriores a `names` realizadas por la clase propietaria. ¿Qué expresión de retorno encaja?
Was this page helpful?