Collezioni non modificabili in Java
Crea collezioni immutabili in Java con List.of, Set.of, Map.of e i wrapper Collections.unmodifiable*.
Una collezione modificabile consente a chiunque abbia un riferimento di modificarne il contenuto. Una collezione non modificabile non lo permette — chiamare add, remove, put, clear o set lancia UnsupportedOperationException. Java offre due modi complementari per crearla: le factory .of(...) introdotte in Java 9 (List.of, Set.of, Map.of, Map.ofEntries) e i più vecchi wrapper Collections.unmodifiable*. Sembrano simili nel punto di chiamata ma si comportano diversamente in due modi importanti, e lo strumento giusto dipende da ciò di cui hai effettivamente bisogno.
Questo capitolo chiude la parte del framework delle collezioni fornendoti una ricetta moderna e pulita per "dammi una costante" e "dammi uno snapshot."
Perché l'immutabilità
Tre vantaggi concreti che rendono il pattern conveniente:
- Condivisione sicura. Passa una lista non modificabile a un costruttore, a un thread worker o a un consumer di eventi e non devi preoccuparti che alterino il tuo stato. Il tipo a livello del compilatore non dice "sola lettura", ma lo fa il runtime.
- Hashable in sicurezza. Inserire una
Listmutabile in unHashSetè un bug — se il contenuto della lista cambia, cambia il suohashCodee il set perde l'elemento. Le collezioni non modificabili evitano completamente questo problema. - Design API migliore. Restituire una vista non modificabile da un getter dice "questo è mio — leggilo, non modificarlo." Senza di esso, ogni chiamante deve decidere se copiare difensivamente.
Le due strategie
List.of, Set.of, Map.of, Map.ofEntries — collezioni veramente immutabili
Aggiunte in Java 9. Costruiscono una nuova collezione con la propria memoria interna. Nient'altro ha un riferimento ad essa:
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)
);Usale per costanti e letterali — piccole collezioni fisse scritte direttamente nel codice. Il JIT le compila in rappresentazioni molto compatte e a basso overhead (spesso un singolo array inline). Il costo è zero per allocazione oltre a quello che il letterale stesso richiede.
Tre vincoli da ricordare:
- Nessun elemento
null, nessuna chiavenull, nessun valorenull.List.of("a", null)lanciaNullPointerExceptionalla costruzione. Se devi rappresentare "assente," usaOptionaloppure ometti la chiave dalla mappa. - Nessun duplicato per
Set.ofeMap.of.Set.of("a", "a")lanciaIllegalArgumentException. Sono pensate per dati letterali che controlli tu. Map.ofha overload solo fino a 10 voci. Per 11 o più, usaMap.ofEntries(Map.entry(...), Map.entry(...), ...).
Collections.unmodifiableList(coll) ecc. — viste di una collezione esistente
Avvolge una collezione in una vista in sola lettura. L'originale è ancora mutabile, e le modifiche attraverso l'originale sono visibili attraverso 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]Usale quando vuoi esporre una collezione interna senza copiarla e senza dare ai chiamanti il permesso di mutarla. Il pattern classico è un getter:
public List<String> getNames() {
return Collections.unmodifiableList(this.names);
}Il chiamante non può modificare this.names attraverso la vista restituita. Tu puoi. Se vuoi impedirlo anche a te stesso, copia:
return List.copyOf(this.names);…che è la terza strategia.
List.copyOf, Set.copyOf, Map.copyOf — snapshot, poi congela
Una scorciatoia per "copia il contenuto corrente in una nuova collezione immutabile":
List<String> snapshot = List.copyOf(mutable);Dopo questa chiamata, snapshot è completamente indipendente da mutable. Le modifiche successive a mutable sono invisibili attraverso snapshot. C'è anche un'ottimizzazione intelligente: se la sorgente è già una collezione non modificabile prodotta da List.of / List.copyOf, la chiamata restituisce la sorgente stessa — zero allocazione.
copyOf rifiuta elementi null, come of. Se la sorgente potrebbe contenere null, usa invece Collections.unmodifiableList(new ArrayList<>(source)).
I tre pattern in sintesi
| Pattern | Indipendente dalla sorgente? | Permette null? | Usare quando |
|---|---|---|---|
List.of("a", "b") | n/a (nessuna sorgente) | No | Costanti letterali |
List.copyOf(source) | Sì — memoria propria | No | Snapshot in un dato momento |
Collections.unmodifiableList(source) | No — vista | Sì | Esporre lo stato interno in sola lettura |
Quando il punto di chiamata recita "questi sono dati letterali hardcoded?" usa of. Quando recita "voglio uno snapshot congelato di ciò che c'è adesso," usa copyOf. Quando recita "voglio che il contenuto corrente sia osservabile ma non modificabile attraverso questo riferimento," usa unmodifiableList.
Superficiale, non profonda
Tutte e tre le strategie sono superficiali — congelano la struttura della collezione, non gli elementi al suo interno.
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}Se vuoi l'immutabilità profonda, devi scegliere tipi di elementi che siano essi stessi immutabili. I record con campi primitivi o String lo sono. I record con campi mutabili non lo sono. Questa è la stessa avvertenza che si applica ai riferimenti final in generale: il binding è fisso, il target potrebbe non esserlo.
Set.of e Map.of hanno ordine di iterazione non specificato
Due scelte di design intenzionali che colgono le persone di sorpresa:
Set.ofeMap.ofrandomizzano deliberatamente l'ordine di iterazione tra le esecuzioni della stessa JVM. Se scrivi codice che dipende da un ordine specifico da queste, vedrai test instabili. UsaList.of(che preserva l'ordine letterale) o unLinkedHashSet/LinkedHashMapavvolto conCollections.unmodifiable*quando hai effettivamente bisogno dell'ordine.Set.of(a, b)eSet.of(b, a)possono iterare diversamente anche nella stessa esecuzione se i valori hanno hash diversi. Non confrontare tramite toString.
Questo è intenzionale — Java ti impedisce di dipendere accidentalmente dall'ordine in modo che l'implementazione sia libera di cambiarlo.
Cosa l'non modificabilità non ti garantisce
- Non è thread-safe per le letture dei campi degli elementi mutabili. Se gli elementi sono mutabili e un altro thread li sta modificando, hai bisogno della sincronizzazione in ogni caso.
- Non rende thread-safe la collezione sottostante.
Collections.unmodifiableList(arrayList)è una vista di una lista non thread-safe; se un altro thread faaddsuarrayList, la lettura attraverso la vista potrebbe vedere uno stato corrotto. Per l'immutabilità thread-safe,List.copyOf(oList.of) è lo strumento giusto — hanno memoria privata. - Non rende
.equalsindipendente dall'ordine. UnaListrestituita daList.ofè ancora uguale per posizione ad altre liste, non per contenuto.
Un esempio pratico: letterali, snapshot, viste e la trappola della superficialità
Il programma seguente mostra tutte e tre le strategie affiancate, dimostra la sorpresa del "la vista vede le mutazioni", la promessa del "la copia è indipendente" e la trappola della superficialità che prende tutti la prima volta.
Cosa trarre dall'esecuzione:
List.ofeList.copyOfproducono entrambi una collezione veramente immutabile — rifiutano ogni mutazione. Differiscono solo nel fatto che i dati siano stati forniti letteralmente o copiati da qualche altra parte.- La vista
Collections.unmodifiableListha rifiutatoview.addma ha accettatobacking.addattraverso il riferimento originale. Le modifiche attraverso la lista di supporto sono diventate visibili attraverso la vista. Questa è la caratteristica distintiva di una vista, e il motivo per cui questa strategia non sostituiscecopyOfnel codice non fidato. - La trappola della superficialità è reale: gli elementi
int[]di unaList<int[]>immutabile sono essi stessi mutabili, e modificarne uno riscrive la lista "congelata." Se vuoi l'immutabilità profonda, i tuoi elementi devono essere già immutabili. Set.ofha rifiutato il duplicato eMap.ofha rifiutato il valorenull— entrambi alla costruzione. Queste collezioni falliscono rapidamente e rumorosamente; è una caratteristica.List.copyOfdi una lista già immutabile ha restituito la stessa istanza senza allocare. Questa è l'ottimizzazione del JDK, e il motivo per cui "copia sempre all'uscita" è economico quando la sorgente è già immutabile.
Cosa segue — e avanti verso la Parte 12
Questo chiude la parte del Collections Framework. Ora conosci ogni implementazione (ArrayList, LinkedList, HashMap, TreeMap, le queue, le deque e il resto), ogni interfaccia (Collection, List, Set, Map, Queue, Deque), i cursori di iterazione (Iterator, ListIterator), le interfacce di ordinamento (Comparable, Comparator), la cassetta degli attrezzi statica (Collections) e la storia dell'immutabilità.
La parte successiva — Programmazione Funzionale — cambia marcia. Invece di come memorizzare i dati, tratta come esprimere trasformazioni sui dati. Il primo capitolo, Programmazione Funzionale in Java, introduce il modello mentale: funzioni come valori, immutabilità, funzioni pure e composizione. Da lì la parte costruisce lambda, riferimenti a metodi, le interfacce funzionali built-in (Function, Predicate, Consumer, Supplier), Optional e gli stream — che usano le collezioni che hai appena imparato come sorgente e destinazione.
La maggior parte dei pattern in questa parte — list.sort(Comparator.comparing(Person::name)), map.getOrDefault(k, 0), stream().filter(...).toList() — sono già di natura funzionale. La Parte 12 rende esplicita questa natura e ti mostra come usarla per tutto il resto.