Por qué colecciones inmutables
Una colección inmutable es una cuyos elementos no pueden modificarse tras su creación: no se puede añadir, eliminar ni sustituir. Esta propiedad trae beneficios reales:
- Thread-safe: múltiples hilos pueden compartir sin sincronización.
- Seguridad: pasarla a APIs no permite que la modifiquen tras tus espaldas.
- Legibilidad: el contrato queda explícito.
- Menos memoria: la JVM aplica optimizaciones (no necesita capacidad extra, estructura interna compacta).
Desde Java 9, la API estándar ofrece factorías elegantes para crearlas sin builders ni bucles.
List.of, Set.of, Map.of
Las factorías estáticas devuelven instancias totalmente inmutables:
List<String> colores = List.of("rojo", "verde", "azul");
Set<Integer> primos = Set.of(2, 3, 5, 7, 11);
Map<String, Integer> edades = Map.of("Ana", 30, "Bob", 25);
Propiedades:
- Inmutables: cualquier intento de modificarlas lanza
UnsupportedOperationException. - No permiten null: ni elementos null, ni claves/valores null. Lanzan
NullPointerException. - Sin duplicados en
Set.ofy claves duplicadas enMap.of: lanzanIllegalArgumentException. - Thread-safe.
- Eficientes: implementaciones especializadas según tamaño (0, 1, 2 o N elementos).
Los elementos sí pueden ser mutables: List.of(new ArrayList<>(...)) tiene una entrada que es una ArrayList modificable. La inmutabilidad es solo de la lista externa.
Map.of hasta 10 entradas
La versión simple de Map.of(k1,v1, k2,v2, ...) acepta hasta 10 entradas:
Map<String, Integer> m = Map.of(
"uno", 1,
"dos", 2,
"tres", 3
);
Para mapas con más entradas, usa Map.ofEntries con Map.entry:
Map<String, Integer> grande = Map.ofEntries(
Map.entry("enero", 1),
Map.entry("febrero", 2),
Map.entry("marzo", 3),
Map.entry("abril", 4),
Map.entry("mayo", 5),
Map.entry("junio", 6),
Map.entry("julio", 7),
Map.entry("agosto", 8),
Map.entry("septiembre", 9),
Map.entry("octubre", 10),
Map.entry("noviembre", 11),
Map.entry("diciembre", 12)
);
List.copyOf, Set.copyOf, Map.copyOf (Java 10+)
Para crear una copia inmutable de una colección existente:
List<String> mutable = new ArrayList<>(List.of("a", "b", "c"));
List<String> inmutable = List.copyOf(mutable);
mutable.add("d");
System.out.println(inmutable); // [a, b, c]: no afectada
Tres optimizaciones importantes:
- Si la colección fuente ya es inmutable del mismo tipo,
copyOfla devuelve tal cual (evita crear una copia innecesaria). - Si contiene nulls, lanza
NullPointerException(mismas restricciones queof). - Usa implementaciones internas compactas.
copyOf es la forma recomendada de aceptar una colección externa y quedarte con una copia inmutable interna (patrón "copia defensiva" para clases inmutables).
Inmutables vs vistas no modificables
Java tiene dos conceptos similares pero distintos:
Vista no modificable (Collections.unmodifiableList(lista)):
- Envuelve una colección existente.
- No se puede modificar a través de la vista, pero si modificas la original, la vista refleja el cambio.
List<String> mutable = new ArrayList<>(List.of("a", "b"));
List<String> vista = Collections.unmodifiableList(mutable);
mutable.add("c"); // OK
System.out.println(vista); // [a, b, c]: sí cambia
vista.add("d"); // UnsupportedOperationException
Inmutable de verdad (List.of, List.copyOf):
- No hay acceso a la fuente original (si se copió).
- Garantiza que el contenido no cambia bajo ninguna circunstancia.
List<String> mutable = new ArrayList<>(List.of("a", "b"));
List<String> copia = List.copyOf(mutable);
mutable.add("c"); // OK en mutable
System.out.println(copia); // [a, b]: NO afectada
copia.add("d"); // UnsupportedOperationException
La regla práctica: para clases inmutables, usa siempre List.copyOf (o Set.copyOf, Map.copyOf). Para vistas de lectura seguras de objetos que sabes que no cambiarán, unmodifiableList puede bastar.
Ejemplos reales
Constantes inmutables en una clase
public class DiasLaborables {
public static final List<DayOfWeek> LABORABLES = List.of(
DayOfWeek.MONDAY,
DayOfWeek.TUESDAY,
DayOfWeek.WEDNESDAY,
DayOfWeek.THURSDAY,
DayOfWeek.FRIDAY
);
public static final Map<String, Integer> MESES_31_DIAS = Map.of(
"enero", 31, "marzo", 31, "mayo", 31, "julio", 31,
"agosto", 31, "octubre", 31, "diciembre", 31
);
}
API que recibe colecciones y las quiere "petrificar"
public final class Pedido {
private final List<LineaPedido> lineas;
public Pedido(List<LineaPedido> lineas) {
this.lineas = List.copyOf(lineas); // inmutable y desconectada
}
public List<LineaPedido> lineas() {
return lineas; // seguro devolverla directamente
}
}
En test fixtures
@Test
void filtrar() {
List<String> entrada = List.of("uno", "dos", "tres", "cuatro");
List<String> esperado = List.of("dos", "tres");
List<String> actual = filtrarPorLongitud(entrada, 3, 4);
assertEquals(esperado, actual);
}
Collectors.toUnmodifiableList/Set/Map (Java 10+)
Para obtener colecciones inmutables directamente desde streams:
List<Integer> pares = numeros.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toUnmodifiableList());
Set<String> nombresUnicos = personas.stream()
.map(Persona::nombre)
.collect(Collectors.toUnmodifiableSet());
Map<String, Integer> indice = productos.stream()
.collect(Collectors.toUnmodifiableMap(Producto::codigo, Producto::stock));
Desde Java 16 existe Stream.toList() que devuelve una lista inmutable por defecto (pero admite null):
List<Integer> pares = numeros.stream()
.filter(n -> n % 2 == 0)
.toList(); // inmutable
Limitaciones
- Sin null: cualquier
nulllanzaNullPointerException. Si necesitas null, usaArrayListtradicional. - Inmutabilidad superficial: los objetos contenidos sí pueden cambiar. Si quieres inmutabilidad profunda, asegúrate de que los elementos también lo sean.
- No se puede modificar: en situaciones donde construyes progresivamente, usa
ArrayListy al final hazList.copyOf.
Rendimiento
Las implementaciones internas de List.of, Set.of y Map.of están optimizadas:
- Tamaño 0: constante compartida (una sola instancia en la JVM).
- Tamaño 1: clase especializada, sin array interno.
- Tamaño 2: dos campos, sin array.
- Tamaño N: array compacto.
Para colecciones pequeñas fijas, son más eficientes que ArrayList o HashMap.
Resumen
| Método | Desde | Inmutable | Copia |
|--------|-------|-----------|-------|
| List.of(...), Set.of(...), Map.of(...) | Java 9 | Sí |: (literal) |
| List.copyOf(x), Set.copyOf(x), Map.copyOf(x) | Java 10 | Sí | Sí (copia) |
| Collectors.toUnmodifiableList() | Java 10 | Sí |: (de stream) |
| Stream.toList() | Java 16 | Sí |: (de stream) |
| Collections.unmodifiableList(lista) | Java 1.2 | Vista (no copia) | No |
| List.of(...) vs Arrays.asList(...) | 9 vs 1.2 | Inmutable vs tamaño fijo |: |
En código moderno, prefiere colecciones inmutables por defecto. Solo usa colecciones mutables cuando realmente necesites modificarlas. Es un cambio de mentalidad que mejora dramáticamente la robustez de cualquier base de código Java.
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Java es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de Java
Explora más contenido relacionado con Java y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Crear colecciones inmutables con factorías .of(...) (Java 9+). Copiar colecciones existentes de forma inmutable con .copyOf (Java 10+). Diferenciar colecciones inmutables de vistas no modificables. Comprender que son thread-safe y eficientes en memoria. Manejar las limitaciones: no se pueden modificar (lanzan excepción). Usar Map.entry para mapas con muchas entradas.