W3docs

Collections non modifiables en Java

Créez des collections immuables en Java avec List.of, Set.of, Map.of et les wrappers Collections.unmodifiable*.

Une collection modifiable permet à quiconque détient une référence d'en modifier le contenu. Une collection non modifiable ne le permet pas — appeler add, remove, put, clear ou set lève une UnsupportedOperationException. Java offre deux façons complémentaires d'en créer une : les factories .of(...) introduites dans Java 9 (List.of, Set.of, Map.of, Map.ofEntries) et les anciens wrappers Collections.unmodifiable*. Ils se ressemblent à l'appel mais se comportent différemment sur deux points importants, et le bon outil dépend de ce dont vous avez réellement besoin.

Ce chapitre clôt la partie sur les collections en vous donnant une recette moderne et claire pour « donner une constante » et « prendre un instantané ».

Pourquoi l'immuabilité

Trois avantages concrets qui valent ce motif :

  1. Partage sécurisé. Passer une liste non modifiable à un constructeur, à un thread de travail ou à un consommateur d'événements vous évite de vous soucier qu'ils mutent votre état. Le type statique ne dit pas « lecture seule », mais le runtime le dit.
  2. Hashable en toute sécurité. Mettre une List mutable dans un HashSet est un bug — si le contenu de la liste change, son hashCode change, et l'ensemble perd l'élément. Les collections non modifiables évitent entièrement ce problème.
  3. Meilleure conception d'API. Retourner une vue non modifiable depuis un getter signifie « c'est à moi — lisez-la, ne la modifiez pas. » Sans cela, chaque appelant doit décider de faire une copie défensive ou non.

Les deux stratégies

List.of, Set.of, Map.of, Map.ofEntries — collections vraiment immuables

Ajoutées dans Java 9. Elles construisent une nouvelle collection avec son propre stockage interne. Personne d'autre n'en détient de référence :

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)
);

Utilisez-les pour les constantes et les littéraux — de petites collections fixes que vous écrivez dans le code. Le JIT les compile en représentations très compactes et peu coûteuses (souvent un seul tableau inline). Le coût est nul par allocation au-delà de ce que le littéral lui-même occupe.

Trois contraintes à retenir :

  1. Pas d'éléments null, pas de clés null, pas de valeurs null. List.of("a", null) lève une NullPointerException à la construction. Si vous avez besoin de représenter « absent », utilisez Optional ou omettez la clé de la map.
  2. Pas de doublons pour Set.of et Map.of. Set.of("a", "a") lève une IllegalArgumentException. Elles sont prévues pour des données littérales que vous contrôlez.
  3. Map.of n'a des surcharges que jusqu'à 10 entrées. Pour 11 ou plus, utilisez Map.ofEntries(Map.entry(...), Map.entry(...), ...).

Collections.unmodifiableList(coll) etc. — vues d'une collection existante

Enveloppez une collection dans une vue en lecture seule. L'original reste mutable, et les modifications apportées via l'original sont visibles à travers la vue :

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]

Utilisez-les quand vous voulez exposer une collection interne sans la copier et sans donner aux appelants la permission de la muter. Le motif classique est un getter :

public List<String> getNames() {
  return Collections.unmodifiableList(this.names);
}

L'appelant ne peut pas modifier this.names via la vue retournée. Vous, vous pouvez. Si vous souhaitez également vous l'interdire, copiez :

return List.copyOf(this.names);

…ce qui constitue la troisième stratégie.

List.copyOf, Set.copyOf, Map.copyOf — instantané, puis gel

Un raccourci pour « copier le contenu actuel dans une nouvelle collection immuable » :

List<String> snapshot = List.copyOf(mutable);

Après cet appel, snapshot est totalement indépendant de mutable. Les modifications ultérieures de mutable sont invisibles à travers snapshot. Il existe également une optimisation astucieuse : si la source est déjà une collection non modifiable produite par List.of / List.copyOf, l'appel retourne la source elle-même — zéro allocation.

copyOf rejette les éléments null, comme of. Si votre source peut contenir null, utilisez plutôt Collections.unmodifiableList(new ArrayList<>(source)).

Les trois motifs en résumé

MotifIndépendant de la source ?Autorise null ?Quand l'utiliser
List.of("a", "b")n/a (pas de source)NonConstantes littérales
List.copyOf(source)Oui — stockage propreNonInstantané à un instant donné
Collections.unmodifiableList(source)Non — vueOuiExposer un état interne en lecture seule

Quand le site d'appel correspond à « ces données sont littéralement codées en dur ? », utilisez of. Quand il correspond à « je veux un instantané figé de ce qui existe maintenant », utilisez copyOf. Quand il correspond à « je veux que le contenu actuel soit observable mais non modifiable via cette référence », utilisez unmodifiableList.

Peu profond, pas profond

Les trois stratégies sont peu profondes — elles figent la structure de la collection, pas les éléments qu'elle contient.

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}

Si vous voulez une immuabilité profonde, vous devez choisir des types d'éléments qui sont eux-mêmes immuables. Les Records avec des champs primitifs ou String le sont. Les Records avec des champs mutables ne le sont pas. C'est la même mise en garde qui s'applique aux références final en général : le lien est fixé, la cible peut ne pas l'être.

Set.of et Map.of ont un ordre d'itération non spécifié

Deux choix de conception intentionnels surprennent les développeurs :

  1. Set.of et Map.of randomisent délibérément l'ordre d'itération entre les exécutions d'une même JVM. Si vous écrivez du code qui dépend d'un ordre spécifique issu de ces méthodes, vous obtiendrez des tests instables. Utilisez List.of (qui préserve l'ordre littéral) ou un LinkedHashSet/LinkedHashMap enveloppé avec Collections.unmodifiable* quand vous avez vraiment besoin de l'ordre.
  2. Set.of(a, b) et Set.of(b, a) peuvent itérer différemment même dans la même exécution si les valeurs ont des hash différents. Ne comparez pas par toString.

C'est intentionnel — Java vous empêche de dépendre accidentellement de l'ordre afin que l'implémentation soit libre de le modifier.

Ce que l'immuabilité ne vous donne pas

  • Ce n'est pas thread-safe pour les lectures de champs d'éléments mutables. Si les éléments sont mutables et qu'un autre thread les modifie, vous avez besoin de synchronisation de toute façon.
  • Cela ne rend pas la collection sous-jacente thread-safe. Collections.unmodifiableList(arrayList) est une vue d'une liste non thread-safe ; si un autre thread fait add sur arrayList, la lecture via la vue peut voir un état corrompu. Pour une immuabilité thread-safe, List.copyOf (ou List.of) est le bon outil — elles possèdent leur propre stockage privé.
  • Cela ne rend pas .equals indépendant de l'ordre. Une List retournée par List.of est toujours égale par position aux autres listes, pas par contenu.

Un exemple pratique : littéraux, instantanés, vues, et le piège de la superficialité

Le programme ci-dessous illustre les trois stratégies côte à côte, démontre la surprise « la vue voit les mutations », la promesse « la copie est indépendante », et le piège de la superficialité qui surprend tout le monde la première fois.

java— editable, runs on the server

Ce qu'il faut retenir de l'exécution :

  • List.of et List.copyOf produisent tous les deux une collection vraiment immuable — elles rejettent toute mutation. Elles ne diffèrent que par la façon dont vous avez fourni les données : littéralement ou en les copiant depuis autre part.
  • La vue Collections.unmodifiableList a rejeté view.add mais a accepté backing.add via la référence originale. Les modifications apportées via la liste de support sont devenues visibles à travers la vue. C'est la caractéristique définissante d'une vue, et la raison pour laquelle cette stratégie ne remplace pas copyOf dans du code non fiable.
  • Le piège de la superficialité est réel : les éléments int[] d'une List<int[]> immuable sont eux-mêmes mutables, et modifier l'un d'eux réécrit la liste « gelée ». Si vous voulez une immuabilité profonde, vos éléments doivent déjà être immuables.
  • Set.of a rejeté le doublon, et Map.of a rejeté la valeur null — tous deux à la construction. Ces collections échouent rapidement et bruyamment ; c'est une fonctionnalité.
  • List.copyOf d'une liste déjà immuable a retourné la même instance sans allouer. C'est l'optimisation du JDK, et c'est pourquoi « toujours copier à la sortie » est peu coûteux quand la source est déjà immuable.

La suite — et vers la Partie 12

Cela clôt la partie Collections Framework. Vous connaissez désormais chaque implémentation (ArrayList, LinkedList, HashMap, TreeMap, les files, les deques et le reste), chaque interface (Collection, List, Set, Map, Queue, Deque), les curseurs d'itération (Iterator, ListIterator), les interfaces d'ordonnancement (Comparable, Comparator), la boîte à outils statique (Collections), et la gestion de l'immuabilité.

La prochaine partie — Functional Programming — change de registre. Au lieu de comment stocker les données, elle traite de comment exprimer des transformations sur les données. Le premier chapitre, Functional Programming in Java, introduit le modèle mental : les fonctions comme valeurs, l'immuabilité, les fonctions pures et la composition. À partir de là, la partie développe les lambdas, les références de méthodes, les interfaces fonctionnelles intégrées (Function, Predicate, Consumer, Supplier), Optional et les streams — qui utilisent les collections que vous venez d'apprendre comme source et destination.

La plupart des motifs de cette partie — list.sort(Comparator.comparing(Person::name)), map.getOrDefault(k, 0), stream().filter(...).toList() — ont déjà une saveur fonctionnelle. La Partie 12 rend cette saveur explicite et vous montre comment l'utiliser pour tout le reste.

Pratique

Pratique
Vous avez un champ `private List<String> names = new ArrayList<>()` et vous voulez un getter qui permet aux appelants de *lire* le contenu actuel sans jamais le muter, et qui reflète également les ajouts ultérieurs à `names` effectués par la classe propriétaire. Quelle expression de retour convient ?
Vous avez un champ `private List<String> names = new ArrayList<>()` et vous voulez un getter qui permet aux appelants de *lire* le contenu actuel sans jamais le muter, et qui reflète également les ajouts ultérieurs à `names` effectués par la classe propriétaire. Quelle expression de retour convient ?
Was this page helpful?