1. Introduzione
Detto in modo semplice, una collezione mutable è quella che puoi modificare dopo la creazione: aggiungere, rimuovere e cambiare elementi. Una collezione immutable (immutabile) è una collezione che, una volta creata, non può essere modificata. Proprio come il cemento dopo l'indurimento: puoi guardarlo, toccarlo, ma non puoi più modellare nuove forme.
Una collezione mutable è come un quaderno con una matita: scrivi, cancelli, aggiungi nuove note. Una collezione immutable è come una pagina plastificata: nessuno potrà più aggiungere o cancellare nulla.
Esempi di collezioni mutabili (mutable)
In Java quasi tutte le collezioni standard sono mutabili per impostazione predefinita. Sono classi come:
- ArrayList
- LinkedList
- HashSet
- TreeSet
- HashMap
- LinkedHashMap
- e molte altre
Esempio: ArrayList
import java.util.*;
List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
names.set(1, "Charlie"); // sostituito Bob con Charlie
names.remove("Alice"); // rimosso Alice
System.out.println(names); // [Charlie]
Qui possiamo fare qualsiasi cosa con la collezione: aggiungere, rimuovere, scambiare elementi. È comodo quando la collezione viene costruita in modo dinamico, ad esempio durante la lettura di dati da un file o l'input dell'utente.
2. Esempi di collezioni immutabili (immutable)
Questi arrivati con Java 9 li avete probabilmente già incontrati, ma richiedono un po' di abitudine:
- List.of(...)
- Set.of(...)
- Map.of(...)
- List.copyOf(collection)
- Set.copyOf(collection)
- Map.copyOf(map)
Esempio: List.of
List<String> planets = List.of("Mercury", "Venus", "Earth", "Mars");
System.out.println(planets); // [Mercury, Venus, Earth, Mars]
planets.add("Jupiter"); // Solleverà UnsupportedOperationException!
Un tentativo di modificare la collezione provoca un'eccezione a runtime.
Esempio: Collections.unmodifiableList
List<String> modifiable = new ArrayList<>(List.of("a", "b"));
List<String> unmodifiable = Collections.unmodifiableList(modifiable);
unmodifiable.add("c"); // UnsupportedOperationException!
Ma qui c'è una trappola: se modifichi la collezione originale, anche il wrapper cambia!
modifiable.add("c");
System.out.println(unmodifiable); // [a, b, c] — l'elemento è comparso!
3. Differenze principali tra collezioni mutable e immutable
| Proprietà | Mutable (mutabili) | Immutable (immutabili) |
|---|---|---|
| Si può aggiungere un elemento? | Sì | No |
| Si può rimuovere un elemento? | Sì | No |
| Si può modificare un elemento? | Sì (per esempio, set) | No |
| Sicurezza rispetto ai thread | No (per impostazione predefinita) | Sì (nessuno stato — niente da cambiare) |
| Si può aggiungere null? | Sì (di solito) | No (nei metodi factory di Java 9+) |
| Implementazioni | ArrayList, HashSet e altre | List.of, Set.of, Map.of, copyOf |
4. Perché servono le collezioni immutabili?
Sorge subito la domanda: se le collezioni mutabili sono così flessibili, perché mai ci servono quelle immutabili? I motivi sono molti e riguardano sicurezza, leggibilità e prevedibilità del codice.
Sicurezza e protezione dagli errori
Quando esponi una collezione all'esterno (ad esempio da un metodo o una classe), vuoi essere sicuro che nessuno ne modifichi per sbaglio il contenuto. È particolarmente importante se la collezione contiene dati «importanti» che non devono cambiare dopo l'inizializzazione.
Esempio:
public class Team {
private final List<String> players;
public Team(List<String> players) {
// Creiamo una copia immutabile affinché nessuno possa alterare la rosa
this.players = List.copyOf(players);
}
public List<String> getPlayers() {
return players;
}
}
Ora qualsiasi codice che ottenga la lista dei giocatori non potrà aggiungerci il proprio «amico».
Sicurezza rispetto ai thread
Le collezioni mutabili non sono sicure quando vi si accede da più thread contemporaneamente. Le collezioni immutabili, invece, possono essere passate liberamente tra thread — nessuno potrà rovinarle.
Semplificazione del debug
Se una collezione non cambia, sai sempre cosa contiene. Non devi temere che qualcuno l'abbia «silenziosamente» modificata altrove nel codice.
Uso come chiavi o valori in altre collezioni
Gli oggetti immutabili sono candidati ideali come chiavi in una Map o elementi in un Set. Se un oggetto può cambiare dopo l'inserimento, rischi di perderne l'accesso (vedi hashCode ed equals).
5. Quando è meglio usare collezioni mutabili?
Le collezioni mutabili sono adatte quando:
- La collezione viene costruita a fasi, in un ciclo o da diverse fonti.
- Sono necessarie modifiche frequenti: aggiunta, rimozione, ordinamento.
- La collezione è destinata all'uso interno e nessuno «all'esterno» potrà rovinarla.
Esempio: costruzione di una lista
List<String> shoppingList = new ArrayList<>();
shoppingList.add("Latte");
shoppingList.add("Pane");
shoppingList.add("Mele");
// Dopo la costruzione — si può creare una versione immutabile
List<String> finalList = List.copyOf(shoppingList);
6. Quando è meglio usare collezioni immutabili?
- Per conservare dati costanti (ad esempio, l'elenco dei giorni della settimana).
- Per passare collezioni tra i layer dell'applicazione (ad esempio, dal DAO al service).
- Per restituire collezioni dai metodi, così da proteggerle da modifiche.
- Per scenari multithread in cui la sicurezza è importante.
Esempio: dati costanti
public static final List<String> WEEKDAYS = List.of(
"Monday", "Tuesday", "Wednesday", "Thursday", "Friday"
);
Esempio: esposizione verso l'esterno
public List<String> getReadOnlyNames() {
return List.copyOf(names); // nessuno potrà modificare la lista
}
7. Caratteristiche e insidie
Immutabilità ≠ sicurezza rispetto ai thread
Una collezione immutabile è protetta dalle modifiche, ma ciò non significa che sia immune da altri problemi in ambienti multithread (ad esempio, se gli elementi della collezione sono oggetti mutabili).
List<List<String>> listOfLists = List.of(new ArrayList<>());
listOfLists.get(0).add("Oops!"); // Si può modificare la lista interna!
Wrapper vs copie
Come già discusso, Collections.unmodifiableList è un wrapper e, se modifichi la collezione di origine, anche il wrapper cambierà. Invece, List.copyOf crea una vera copia indipendente.
List<String> base = new ArrayList<>(List.of("a", "b"));
List<String> wrap = Collections.unmodifiableList(base);
List<String> copy = List.copyOf(base);
base.add("c");
System.out.println(wrap); // [a, b, c] — è cambiata!
System.out.println(copy); // [a, b] — è rimasta uguale!
NullPointerException
I metodi factory (List.of, Set.of, Map.of) non consentono di aggiungere null:
List<String> bad = List.of("a", null); // Solleverà NullPointerException già in fase di creazione!
8. Confronto degli approcci: pro e contro
Collezioni mutabili
Il principale vantaggio delle collezioni mutabili è la loro flessibilità. Puoi aggiungere e rimuovere elementi al volo, ristrutturare la collezione man mano che evolve la logica del programma. Questo approccio è particolarmente comodo quando devi assemblare rapidamente una struttura temporanea o modificare dinamicamente qualcosa.
Ma la comodità ha un prezzo. Una collezione mutabile porta sempre con sé il rischio di interventi accidentali: qualcuno nel codice potrebbe modificare i dati senza volerlo, provocando bug difficili da individuare. Nei programmi multithread la situazione è ancora peggiore — modifiche parallele possono portare facilmente a race condition e malfunzionamenti inattesi. Anche il controllo del ciclo di vita di questi dati è più complesso: bisogna ricordare costantemente chi e quando può modificare la collezione.
Collezioni immutabili
Con le collezioni immutabili si lavora con maggiore tranquillità. Sai con certezza che nessuno le modificherà «alle tue spalle». Questo rende il codice più sicuro, più semplice da fare debug e da testare, e le collezioni stesse sono comode da passare tra thread senza blocchi aggiuntivi.
Lo svantaggio è che talvolta occorre sacrificare un po' di performance. Se devi aggiungere un nuovo elemento, devi creare una nuova copia della collezione, con un costo addizionale in termini di memoria e tempo. Inoltre, costruire grandi e complesse strutture direttamente in forma immutabile può essere scomodo. Di solito si costruiscono in una collezione mutabile temporanea e, quando tutto è pronto, si «congelano».
9. Errori tipici
Errore № 1: Restituire all'esterno una collezione mutabile. Se restituisci da un metodo un normale ArrayList, qualsiasi codice esterno può aggiungere o rimuovere elementi. Questo può portare a bug molto difficili da tracciare.
Errore № 2: Usare un wrapper invece di una copia. Se usi Collections.unmodifiableList, ma la collezione di origine viene modificata altrove, l'«immutabilità» è un'illusione.
Errore № 3: Oggetti mutabili all'interno di collezioni immutabili. Anche se la collezione è immutabile, i suoi elementi possono essere mutabili. Questo può portare a cambiamenti di stato inattesi.
Errore № 4: Tentare di aggiungere null a una collezione creata tramite metodi factory. A differenza delle vecchie collezioni, i nuovi metodi factory non consentono di aggiungere null — otterrai una NullPointerException.
Errore № 5: Fare affidamento su una specifica implementazione. Le collezioni create con List.of o Set.of non garantiscono il tipo di implementazione (non è necessariamente ArrayList o HashSet). Non farci affidamento.
GO TO FULL VERSION