1. Le problème de la mutabilité des collections
En Java, les collections ressemblent à un entrepôt de marchandises : n’importe qui peut venir ajouter, enlever, modifier quelque chose. Parfois c’est pratique, mais dans les grands programmes cela devient un casse-tête. Imaginez que vous ayez transmis une liste de produits depuis votre classe vers l’extérieur, et que quelqu’un en supprime la moitié. Ou — encore plus « amusant » — dans une application multithread, un thread ajoute des éléments pendant qu’un autre les lit : le résultat peut être inattendu et les erreurs difficiles à traquer (par exemple, ConcurrentModificationException).
Voici un exemple montrant pourquoi la mutabilité des collections est une source de bogues :
import java.util.*;
public class Inventory {
private List<String> products = new ArrayList<>();
public Inventory() {
products.add("Thé");
products.add("Café");
}
public List<String> getProducts() {
// DANGEREUX ! Nous renvoyons une référence à la liste interne
return products;
}
}
public class Main {
public static void main(String[] args) {
Inventory inv = new Inventory();
List<String> external = inv.getProducts();
external.remove("Thé"); // Oups! Il n'y a plus de thé dans l'inventaire
System.out.println(inv.getProducts()); // [Café]
}
}
Vous voyez le piège ? Une méthode renvoie la collection interne, une autre la modifie. On peut ainsi détruire par inadvertance des données qui devraient être protégées.
2. Créer des collections immuables : Collections.unmodifiable*
Pour éviter ce genre de désagréments, Java propose une protection efficace : on peut rendre une collection « non modifiable » via des vues spéciales fournies par la classe Collections :
- Collections.unmodifiableList(list)
- Collections.unmodifiableSet(set)
- Collections.unmodifiableMap(map)
Comment cela fonctionne-t-il ? Vous créez d’abord une collection ordinaire, puis vous l’enveloppez dans une vue « non modifiable » :
import java.util.*;
public class Main {
public static void main(String[] args) {
List<String> drinks = new ArrayList<>();
drinks.add("Thé");
drinks.add("Café");
List<String> immutableDrinks = Collections.unmodifiableList(drinks);
System.out.println(immutableDrinks); // [Thé, Café]
// Essayons d'ajouter un élément
immutableDrinks.add("Cacao"); // Bam! UnsupportedOperationException
}
}
Toute tentative de modifier une telle collection provoque le déclenchement d’une UnsupportedOperationException. C’est comme si vous colliez une énorme étiquette "NE PAS TOUCHER!" sur la boîte — et toute personne qui essaiera d’ajouter ou de supprimer quelque chose se fera taper sur les doigts (ou sur la pile d’appels).
Exemple : protéger l’état interne
Corrigeons notre classe Inventory de l’exemple précédent :
import java.util.*;
public class Inventory {
private List<String> products = new ArrayList<>();
public Inventory() {
products.add("Thé");
products.add("Café");
}
public List<String> getProducts() {
// Désormais, nous renvoyons une vue
return Collections.unmodifiableList(products);
}
}
Désormais, si quelqu’un essaie de modifier la liste obtenue, il obtiendra une exception.
3. Comportement des collections immuables : protection superficielle
Important à comprendre : unmodifiableList et ses « frères » ne font qu’ajouter une vue autour de la collection d’origine. Elles ne créent pas de copie — toute modification de la collection d’origine (celle « à l’intérieur ») sera visible aussi dans la vue !
Démonstration
import java.util.*;
public class Main {
public static void main(String[] args) {
List<String> drinks = new ArrayList<>();
drinks.add("Thé");
List<String> immutableDrinks = Collections.unmodifiableList(drinks);
drinks.add("Café"); // Nous modifions la collection d'origine
System.out.println(immutableDrinks); // [Thé, Café] — l'élément est apparu !
}
}
Conclusion : la vue protège seulement contre les modifications via la vue elle-même. Si quelqu’un détient une référence à la collection d’origine, il peut toujours la modifier.
4. Immuabilité profonde : mythes et réalités
Les vues unmodifiable* rendent la collection non modifiable seulement de l’extérieur. Mais si la collection contient des objets mutables, on peut les modifier !
Exemple
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("Thé"));
List<Product> immutableProducts = Collections.unmodifiableList(products);
// Nous modifions l'objet contenu dans la collection
immutableProducts.get(0).name = "Café";
System.out.println(immutableProducts); // [Café]
}
}
Conclusion :
- La collection est « immuable », mais pas les objets qu’elle contient.
- Pour une immuabilité complète (profonde), utilisez des objets immuables (par exemple, String, Integer, record-classes, ou rendez vos propres classes immuables).
5. Quand utiliser des collections immuables
Pour protéger l’état interne
Si vous écrivez une classe qui stocke une collection et que vous l’exposez à l’extérieur, renvoyez toujours une vue afin que personne ne puisse modifier vos données par accident (ou volontairement) :
public List<String> getProducts() {
return Collections.unmodifiableList(products);
}
Dans les programmes multithread
Dans les applications multithread, les collections mutables sont une source de problèmes (race condition, ConcurrentModificationException et autres « joies de la vie »). Si la collection n’a pas besoin d’être modifiée après sa création, rendez-la immuable.
Pour transmettre des données entre couches
Si vous transmettez une collection d’une couche de l’application à une autre (par exemple, d’un DAO à un service), passez une copie immuable ou une vue — cela protège contre les modifications accidentelles.
6. Exemples pratiques
Exemple 1 : protéger la liste des étudiants
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);
}
}
Ainsi, personne ne pourra ajouter ou supprimer un étudiant directement via getStudents().
Exemple 2 : carte (Map) immuable
import java.util.*;
public class Main {
public static void main(String[] args) {
Map<String, Integer> grades = new HashMap<>();
grades.put("John", 5);
grades.put("Mary", 4);
Map<String, Integer> immutableGrades = Collections.unmodifiableMap(grades);
// immutableGrades.put("Peter", 3); // UnsupportedOperationException
}
}
7. Subtilités utiles
Alternatives modernes : List.of, Set.of, Map.of
Avec Java 9, des moyens encore plus pratiques de créer des collections immuables sont apparus :
List<String> drinks = List.of("Thé", "Café");
Set<String> fruits = Set.of("Pomme", "Banane");
Map<String, Integer> ages = Map.of("John", 20, "Mary", 21);
- Ces collections sont immuables (toute tentative de modification — exception).
- Elles n’ont pas de collection « d’origine » mutable (contrairement à Collections.unmodifiable*).
- Elles n’acceptent pas la valeur null.
Avec Java 10, il existe aussi des méthodes de copie : List.copyOf, Set.copyOf, Map.copyOf — elles créent une copie immuable de la collection fournie.
Comparaison des façons de créer des collections immuables
| Méthode | Immuabilité profonde | Peut-on modifier la collection d'origine ? | Accepte null ? | Version de Java |
|---|---|---|---|---|
|
Non | Oui | Oui | 1.2 |
|
Non | Non (pas de collection d'origine) | Non | 9+ |
8. Erreurs courantes avec les collections immuables
Erreur n° 1 : modifier la collection d’origine après avoir créé la vue. Vous avez créé une unmodifiableList, puis quelqu’un modifie la liste d’origine. La vue ne protège pas contre cela — les modifications seront visibles partout où la vue est utilisée.
Erreur n° 2 : s’attendre à une immuabilité profonde. Beaucoup pensent que si la collection est immuable, les objets qu’elle contient ne peuvent pas être modifiés non plus. En réalité, seule la structure est protégée (ajout/suppression/modification via la collection), pas le contenu des objets.
Erreur n° 3 : utiliser des collections immuables avec la valeur null dans les usines modernes. Les collections créées via List.of, Set.of, Map.of n’acceptent pas null. Toute tentative d’ajouter ou de recevoir null entraînera une exception.
Erreur n° 4 : exposer des références à des collections mutables. Si vous renvoyez à l’extérieur une référence à une collection interne (sans vue), vous perdez le contrôle de vos données — voie directe vers les bogues et la violation des invariants.
Erreur n° 5 : utiliser des collections immuables dans du code qui s’attend à de la mutabilité. Si du code tiers essaie de modifier la collection (par exemple, ajouter un élément), il obtiendra une UnsupportedOperationException. Assurez-vous que le code client est au courant de l’immuabilité des données.
GO TO FULL VERSION