W3docs

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:

  1. 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.
  2. Hashable in sicurezza. Inserire una List mutabile in un HashSet è un bug — se il contenuto della lista cambia, cambia il suo hashCode e il set perde l'elemento. Le collezioni non modificabili evitano completamente questo problema.
  3. 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:

  1. Nessun elemento null, nessuna chiave null, nessun valore null. List.of("a", null) lancia NullPointerException alla costruzione. Se devi rappresentare "assente," usa Optional oppure ometti la chiave dalla mappa.
  2. Nessun duplicato per Set.of e Map.of. Set.of("a", "a") lancia IllegalArgumentException. Sono pensate per dati letterali che controlli tu.
  3. Map.of ha overload solo fino a 10 voci. Per 11 o più, usa Map.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

PatternIndipendente dalla sorgente?Permette null?Usare quando
List.of("a", "b")n/a (nessuna sorgente)NoCostanti letterali
List.copyOf(source)Sì — memoria propriaNoSnapshot in un dato momento
Collections.unmodifiableList(source)No — vistaEsporre 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:

  1. Set.of e Map.of randomizzano 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. Usa List.of (che preserva l'ordine letterale) o un LinkedHashSet/LinkedHashMap avvolto con Collections.unmodifiable* quando hai effettivamente bisogno dell'ordine.
  2. Set.of(a, b) e Set.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 fa add su arrayList, la lettura attraverso la vista potrebbe vedere uno stato corrotto. Per l'immutabilità thread-safe, List.copyOf (o List.of) è lo strumento giusto — hanno memoria privata.
  • Non rende .equals indipendente dall'ordine. Una List restituita da List.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.

java— editable, runs on the server

Cosa trarre dall'esecuzione:

  • List.of e List.copyOf producono 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.unmodifiableList ha rifiutato view.add ma ha accettato backing.add attraverso 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 sostituisce copyOf nel codice non fidato.
  • La trappola della superficialità è reale: gli elementi int[] di una List<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.of ha rifiutato il duplicato e Map.of ha rifiutato il valore null — entrambi alla costruzione. Queste collezioni falliscono rapidamente e rumorosamente; è una caratteristica.
  • List.copyOf di 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.

Pratica

Pratica
Hai un campo `private List<String> names = new ArrayList<>()` e vuoi un getter che consenta ai chiamanti di *leggere* il contenuto corrente ma senza mai mutarlo, e che rifletta anche le aggiunte successive a `names` effettuate dalla classe proprietaria. Quale espressione di ritorno è adatta?
Hai un campo `private List<String> names = new ArrayList<>()` e vuoi un getter che consenta ai chiamanti di *leggere* il contenuto corrente ma senza mai mutarlo, e che rifletta anche le aggiunte successive a `names` effettuate dalla classe proprietaria. Quale espressione di ritorno è adatta?
Was this page helpful?