1. Wrappers non modifiables : enveloppes autour des collections
Parfois, il existe déjà dans le code une collection que quelqu’un peut modifier par accident (ou pas). Par exemple, vous avez une liste d’utilisateurs que vous souhaitez exposer vers l’extérieur, sans permettre à quiconque de la modifier :
List<String> users = new ArrayList<>();
users.add("Alice");
users.add("Bob");
Vous retournez cette liste depuis une méthode, et quelqu’un fait users.add("Hacker") ; — et voilà qu’un nouvel utilisateur non autorisé apparaît dans votre système ! Comment s’en protéger ?
Enveloppes via Collections
En Java, il existe depuis longtemps des méthodes d’enveloppement dans la classe Collections :
- Collections.unmodifiableList(list)
- Collections.unmodifiableSet(set)
- Collections.unmodifiableMap(map)
Ces méthodes retournent une enveloppe autour de votre collection, qui ne permet pas de la modifier via elle. Toute tentative d’ajout, de suppression ou de remplacement d’un élément via l’enveloppe lèvera une UnsupportedOperationException.
Exemple :
import java.util.*;
public class Demo {
public static void main(String[] args) {
List<String> modifiable = new ArrayList<>();
modifiable.add("Alice");
modifiable.add("Bob");
// Créer une enveloppe non modifiable
List<String> unmodifiable = Collections.unmodifiableList(modifiable);
System.out.println(unmodifiable); // [Alice, Bob]
// Essayons d'ajouter un élément via l'enveloppe
try {
unmodifiable.add("Charlie"); // Boum ! UnsupportedOperationException
} catch (UnsupportedOperationException e) {
System.out.println("Impossible de modifier la collection : " + e);
}
}
}
Important !
- L’enveloppe NE rend PAS la collection d’origine immuable. Si quelqu’un garde une référence vers la collection d’origine, il peut toujours la modifier.
- Toutes les modifications de la collection d’origine sont visibles via l’enveloppe.
modifiable.add("Charlie");
System.out.println(unmodifiable); // [Alice, Bob, Charlie]
Autrement dit, si quelqu’un, quelque part dans le code, ajoute/supprime un élément dans la liste originale, l’enveloppe le verra. Ce n’est pas un « gel », juste une interdiction de modifier via l’enveloppe elle-même.
Enveloppes pour d’autres collections
De la même manière, on peut créer des enveloppes pour Set, Map, et même pour des structures plus exotiques :
Set<Integer> numbers = new HashSet<>(Set.of(1, 2, 3));
Set<Integer> unmodSet = Collections.unmodifiableSet(numbers);
Map<String, Integer> ages = new HashMap<>();
ages.put("Alice", 30);
ages.put("Bob", 25);
Map<String, Integer> unmodMap = Collections.unmodifiableMap(ages);
Comparaison avec les méthodes usine (List.of et autres)
- List.of(...) crée une nouvelle collection immuable, dans laquelle on ne peut pas ajouter, même au départ.
- Collections.unmodifiableList(list) — c’est une enveloppe autour d’une collection existante. Si la liste d’origine change, l’enveloppe change aussi.
Tableau : comparaison des approches
|
|
|
|---|---|---|
| Peut-on ajouter ? | Non | Non (via l’enveloppe) |
| Peut-on ajouter dans la collection d’origine ? | Sans objet | Oui |
| Modifications visibles ? | Non | Oui |
| Peut-on mettre null ? | Non (NPE) | Oui (si la collection d’origine l’autorise) |
| Implémentation | Propre | Enveloppe autour de votre collection |
2. Collections CopyOnWrite
Dans les programmes multithread, on rencontre souvent le cas suivant : un thread (ou plusieurs) lit la collection, tandis qu’un autre (ou d’autres) la modifie parfois. Les collections ordinaires ne conviennent pas ici : risques de data races, erreurs, ConcurrentModificationException et autres joies du monde multithread.
Pour ces cas, on a inventé les collections CopyOnWrite : elles sont conçues pour les scénarios où les lectures sont fréquentes et les modifications rares.
Comment ça fonctionne ?
- À chaque modification (ajout, suppression, remplacement), la collection crée une nouvelle copie du tableau interne.
- Tous les threads lecteurs obtiennent leur « propre » version du tableau, qui ne change pas pendant leur lecture.
- Cela rend la lecture absolument sûre et ne nécessite pas de synchronisation.
Classes principales
- CopyOnWriteArrayList<E>
- CopyOnWriteArraySet<E>
Elles se trouvent dans le paquet java.util.concurrent.
Exemple d’utilisation
import java.util.concurrent.CopyOnWriteArrayList;
public class CopyOnWriteDemo {
public static void main(String[] args) {
CopyOnWriteArrayList<String> cowList = new CopyOnWriteArrayList<>();
cowList.add("Alpha");
cowList.add("Beta");
// Itération sûre, même si quelqu'un ajoute des éléments en parallèle
for (String s : cowList) {
System.out.println(s);
cowList.add("Gamma"); // Ne lèvera pas ConcurrentModificationException !
}
System.out.println(cowList); // [Alpha, Beta, Gamma, Gamma]
}
}
Particularités :
- L’itérateur des collections CopyOnWrite « voit » toujours un instantané de la collection au moment de sa création.
- Si des éléments sont ajoutés après la création de l’itérateur, celui-ci ne les verra pas.
- On peut ajouter/supprimer des éléments en toute sécurité pendant l’itération — aucune ConcurrentModificationException !
Quand utiliser les collections CopyOnWrite ?
Elles conviennent aux situations où de nombreux threads lisent principalement les données de la collection, tandis que les opérations de modification sont très rares. Exemple classique : la liste des auditeurs d’événements (event listeners) : on ajoute ou supprime de nouveaux auditeurs peu souvent, mais la notification de ces auditeurs se fait en permanence.
Exemple — abonnés aux événements
import java.util.concurrent.CopyOnWriteArrayList;
public class EventBus {
private final CopyOnWriteArrayList<Runnable> listeners = new CopyOnWriteArrayList<>();
public void subscribe(Runnable listener) {
listeners.add(listener);
}
public void publishEvent() {
for (Runnable listener : listeners) {
listener.run(); // sûr, même si quelqu'un vient de s'abonner/se désabonner !
}
}
}
Inconvénients des collections CopyOnWrite
- Lent pour des modifications fréquentes : chaque modification implique la création d’une nouvelle copie du tableau, ce qui est coûteux en mémoire et en temps.
- Inefficace pour de grandes collections : si la collection est volumineuse, la copie du tableau est une opération coûteuse.
3. Comparaison : quand utiliser quoi ?
Wrappers non modifiables (Collections.unmodifiable...)
Quand les utiliser : Quand vous avez déjà une collection que vous souhaitez protéger des modifications via du code externe, mais que les modifications internes (le propriétaire de la collection) sont acceptables.
Sécurité des threads : Non garantie ! Si la collection d’origine est modifiée depuis un autre thread, il peut y avoir des data races et des erreurs.
Méthodes usine (List.of, Set.of, Map.of)
Quand les utiliser : Quand vous voulez créer immédiatement une collection constante immuable, sans aucune possibilité de modification d’où que ce soit.
Sécurité des threads : Garantie (la collection ne change jamais).
Collections CopyOnWrite
Quand les utiliser : Dans des scénarios multithread avec beaucoup de lectures et peu de modifications. Par exemple, pour des listes d’abonnés.
Sécurité des threads : Oui, entièrement sûres pour les threads.
Immuabilité : Non, la collection peut être modifiée, mais une nouvelle copie est créée à chaque fois afin de ne pas perturber les lecteurs.
4. Erreurs typiques et particularités d’implémentation
Erreur n° 1 : Attendre un « gel » de la collection d’origine via une enveloppe. Beaucoup pensent que Collections.unmodifiableList(list) rend la collection totalement immuable. En réalité, si quelqu’un conserve une référence vers la liste originale, il peut la modifier, et ces modifications seront visibles via l’enveloppe. Solution : Si vous avez besoin d’une vraie immuabilité, utilisez List.copyOf(list) (Java 10+) ou List.of(...).
Erreur n° 2 : Utiliser CopyOnWrite pour une collection fréquemment modifiée. Si l’on ajoute ou supprime constamment des éléments dans une CopyOnWriteArrayList, cela entraînera des problèmes de performance et de mémoire. CopyOnWrite n’est adapté qu’aux scénarios « beaucoup de lecteurs, peu d’écrivains ».
Erreur n° 3 : Penser que les enveloppes sont thread-safe. Collections.unmodifiableList ne rend pas la collection sûre pour les threads ! Si la liste d’origine est modifiée depuis différents threads, des erreurs sont possibles.
Erreur n° 4 : Utiliser les collections de List.of ou Set.of avec null. Contrairement aux collections ordinaires, les méthodes usine n’acceptent pas null — tenter d’ajouter ou même de créer une collection avec null entraînera une NullPointerException.
GO TO FULL VERSION