1. Il problema della mutabilità delle collezioni
In Java le collezioni ricordano un magazzino di merce: chiunque può venire e aggiungere, togliere, cambiare qualcosa. A volte è comodo, ma nelle applicazioni grandi diventa un mal di testa. Immaginate di aver passato una lista di prodotti dal vostro class all’esterno, e qualcuno ha preso e rimosso metà delle voci. Oppure — ancora più divertente — in un programma multithread un thread aggiunge elementi e un altro li legge: il risultato può essere inatteso e gli errori difficili da intercettare (per esempio, ConcurrentModificationException).
Ecco un esempio del perché la mutabilità delle collezioni è fonte di bug:
import java.util.*;
public class Inventory {
private List<String> products = new ArrayList<>();
public Inventory() {
products.add("Tè");
products.add("Caffè");
}
public List<String> getProducts() {
// PERICOLOSO! Restituiamo un riferimento alla lista interna
return products;
}
}
public class Main {
public static void main(String[] args) {
Inventory inv = new Inventory();
List<String> external = inv.getProducts();
external.remove("Tè"); // Ops! Ora nell'inventario non c'è più tè
System.out.println(inv.getProducts()); // [Caffè]
}
}
Avete notato la trappola? Un metodo restituisce la collezione interna, un altro la modifica. Così si possono distruggere per errore dati che dovrebbero essere protetti.
2. Creare collezioni immutabili: Collections.unmodifiable*
Per evitare simili inconvenienti, Java offre una protezione efficace: si può rendere una collezione «immutabile» con speciali wrapper della classe Collections:
- Collections.unmodifiableList(list)
- Collections.unmodifiableSet(set)
- Collections.unmodifiableMap(map)
Come funziona? Prima create una normale collezione, poi la avvolgete in un guscio «immutabile»:
import java.util.*;
public class Main {
public static void main(String[] args) {
List<String> drinks = new ArrayList<>();
drinks.add("Tè");
drinks.add("Caffè");
List<String> immutableDrinks = Collections.unmodifiableList(drinks);
System.out.println(immutableDrinks); // [Tè, Caffè]
// Proviamo ad aggiungere un elemento
immutableDrinks.add("Cacao"); // Boom! UnsupportedOperationException
}
}
Il tentativo di modificare tale collezione provoca un’eccezione UnsupportedOperationException. È come attaccare sulla scatola un enorme adesivo "NON TOCCARE!" — e chiunque provi ad aggiungere o rimuovere qualcosa, si farà male alle mani (o allo stack delle chiamate).
Esempio: proteggiamo lo stato interno
Correggiamo la nostra classe Inventory dall’esempio precedente:
import java.util.*;
public class Inventory {
private List<String> products = new ArrayList<>();
public Inventory() {
products.add("Tè");
products.add("Caffè");
}
public List<String> getProducts() {
// Ora restituiamo il wrapper
return Collections.unmodifiableList(products);
}
}
Ora, se qualcuno prova a modificare la lista ottenuta, riceverà un’eccezione.
3. Comportamento delle collezioni immutabili: protezione superficiale
È importante capire: unmodifiableList e i suoi «fratelli» fanno solo un wrapper attorno alla collezione di origine. Non creano una copia — qualsiasi modifica della collezione originale (quella «interna») sarà visibile anche nel wrapper!
Dimostrazione
import java.util.*;
public class Main {
public static void main(String[] args) {
List<String> drinks = new ArrayList<>();
drinks.add("Tè");
List<String> immutableDrinks = Collections.unmodifiableList(drinks);
drinks.add("Caffè"); // Modifichiamo la collezione originale
System.out.println(immutableDrinks); // [Tè, Caffè] — l'elemento è comparso!
}
}
Conclusione: il wrapper protegge solo dalle modifiche attraverso il wrapper stesso. Se qualcuno detiene un riferimento alla collezione originale, può comunque modificarla.
4. Immutabilità profonda: miti e realtà
I wrapper unmodifiable* rendono la collezione immutabile solo dall’esterno. Ma se la collezione contiene oggetti mutabili, questi possono essere modificati!
Esempio
import java.util.*;
class Product {
String name;
Product(String name) {
this.name = name;
}
public String toString() {
return name;
}
}
public class Main {
public static void main(String[] args) {
List<Product> products = new ArrayList<>();
products.add(new Product("Tè"));
List<Product> immutableProducts = Collections.unmodifiableList(products);
// Modifichiamo l'oggetto dentro la collezione
immutableProducts.get(0).name = "Caffè";
System.out.println(immutableProducts); // [Caffè]
}
}
Conclusione:
- La collezione è «immutabile», ma gli oggetti al suo interno no.
- Per una vera (profonda) immutabilità usate oggetti immutabili (per esempio, String, Integer, classi record o rendete immutabili le vostre classi).
5. Quando usare le collezioni immutabili
Per proteggere lo stato interno
Se scrivete una classe che conserva una collezione e la esponete all’esterno, restituite sempre un wrapper affinché nessuno possa modificare per errore (o di proposito) i vostri dati:
public List<String> getProducts() {
return Collections.unmodifiableList(products);
}
Nelle applicazioni multithread
Nelle applicazioni multithread le collezioni mutabili sono fonte di problemi (race condition, ConcurrentModificationException e altre «gioie della vita»). Se la collezione non va modificata dopo la creazione, rendetela immutabile.
Per passare dati tra layer
Se passate una collezione da un layer dell’applicazione a un altro (per esempio, dal DAO al service), passate una copia immutabile o un wrapper — protegge da modifiche accidentali.
6. Esempi pratici
Esempio 1: proteggiamo l’elenco degli studenti
import java.util.*;
public class Group {
private final List<String> students = new ArrayList<>();
public void addStudent(String name) {
students.add(name);
}
public List<String> getStudents() {
return Collections.unmodifiableList(students);
}
}
Ora nessuno potrà aggiungere o rimuovere studenti direttamente tramite getStudents().
Esempio 2: mappa immutabile (Map)
import java.util.*;
public class Main {
public static void main(String[] args) {
Map<String, Integer> grades = new HashMap<>();
grades.put("Vasya", 5);
grades.put("Masha", 4);
Map<String, Integer> immutableGrades = Collections.unmodifiableMap(grades);
// immutableGrades.put("Petya", 3); // UnsupportedOperationException
}
}
7. Dettagli utili
Alternative moderne: List.of, Set.of, Map.of
In Java 9 sono apparsi modi ancora più comodi per creare collezioni immutabili:
List<String> drinks = List.of("Tè", "Caffè");
Set<String> fruits = Set.of("Mela", "Banana");
Map<String, Integer> ages = Map.of("Vasya", 20, "Masha", 21);
- Queste collezioni sono immutabili (qualsiasi tentativo di modificarle — eccezione).
- Non hanno una collezione «originale» mutabile (a differenza di Collections.unmodifiable*).
- Non ammettono il valore null.
Inoltre, da Java 10 ci sono i metodi di copia: List.copyOf, Set.copyOf, Map.copyOf — creano una copia immutabile della collezione passata.
Confronto dei modi per creare collezioni immutabili
| Metodo | Immutabilità profonda | Si può modificare la collezione originale? | Ammette null? | Versione di Java |
|---|---|---|---|---|
|
No | Sì | Sì | 1.2 |
|
No | No (nessuna collezione originale) | No | 9+ |
8. Errori tipici nell’uso delle collezioni immutabili
Errore n. 1: modificare la collezione originale dopo aver creato il wrapper. Avete creato unmodifiableList, e poi qualcuno modifica la lista originale. Il wrapper non vi proteggerà da questo — le modifiche saranno visibili ovunque venga usato il wrapper.
Errore n. 2: aspettarsi l’immutabilità profonda. Molti pensano che se la collezione è immutabile, anche gli oggetti al suo interno non possano essere modificati. In realtà è protetta solo la struttura (aggiunta/rimozione/modifica tramite la collezione), non il contenuto degli oggetti.
Errore n. 3: usare collezioni immutabili con il valore null nelle factory moderne. Le collezioni create tramite List.of, Set.of, Map.of non ammettono null. Tentare di aggiungere o ottenere null porterà a un’eccezione.
Errore n. 4: passare riferimenti a collezioni mutabili all’esterno. Se restituite all’esterno un riferimento alla collezione interna (senza wrapper), perdete il controllo sui vostri dati — strada diretta verso bug e violazione degli invarianti.
Errore n. 5: usare collezioni immutabili in codice che si aspetta mutabilità. Se codice di terze parti prova a modificare la collezione (per esempio, aggiungere un elemento), otterrà UnsupportedOperationException. Assicuratevi che i consumatori sappiano dell’immutabilità dei dati.
GO TO FULL VERSION