Table of Contents
Introduction
L’immutabilite est un concept fondamental de la programmation fonctionnelle qui gagne en importance dans le developpement Java moderne. Depuis Java 9, le langage offre des methodes factory elegantes pour creer des collections immuables : List.of(), Set.of() et Map.of().
Pourquoi utiliser des collections immuables ?
Les collections immuables offrent plusieurs avantages majeurs :
- Thread-safety garantie : Aucun risque de modification concurrente
- Securite du code : Les donnees ne peuvent pas etre alterees accidentellement
- Performance : Optimisations possibles car l’etat ne change jamais
- Previsibilite : Le comportement du code est plus facile a comprendre et a debugger
- Partage sur : Vous pouvez partager des references sans crainte de modifications
Dans cet article, nous explorerons en profondeur les collections immuables natives de Java, les bibliotheques tierces comme Guava et Eclipse Collections, ainsi que les Multimaps pour gerer des associations cle-valeurs multiples.
Collections Immuables en Java 9+
Les collections immuables sont des instances qui ne peuvent pas etre modifiees apres leur creation. Toute tentative de modification leve une UnsupportedOperationException.
List.of() - Listes Immuables
La methode List.of() cree une liste immuable avec un ordre d’iteration garanti.
import java.util.List;
public class ImmutableListExample {
public static void main(String[] args) {
// Liste vide
List<String> emptyList = List.of();
// Liste avec des elements
List<String> fruits = List.of("Pomme", "Banane", "Orange", "Kiwi");
// Acces aux elements (lecture seule)
System.out.println(fruits.get(0)); // Pomme
System.out.println(fruits.size()); // 4
// Iteration
for (String fruit : fruits) {
System.out.println(fruit);
}
// ATTENTION : Ceci leve une UnsupportedOperationException
// fruits.add("Mangue"); // ERREUR !
// fruits.remove(0); // ERREUR !
// fruits.set(0, "Poire"); // ERREUR !
}
}
Set.of() - Ensembles Immuables
La methode Set.of() cree un ensemble immuable sans doublons.
import java.util.Set;
public class ImmutableSetExample {
public static void main(String[] args) {
// Ensemble vide
Set<Integer> emptySet = Set.of();
// Ensemble avec des elements uniques
Set<Integer> numbers = Set.of(1, 2, 3, 4, 5);
// Verification de presence
System.out.println(numbers.contains(3)); // true
System.out.println(numbers.contains(10)); // false
// ATTENTION : Les doublons levent une IllegalArgumentException
// Set<Integer> invalid = Set.of(1, 2, 2); // ERREUR a l'execution !
}
}
Map.of() et Map.ofEntries() - Maps Immuables
Pour les maps, Java 9+ offre deux methodes selon le nombre d’entrees.
import java.util.Map;
import static java.util.Map.entry;
public class ImmutableMapExample {
public static void main(String[] args) {
// Map vide
Map<String, Integer> emptyMap = Map.of();
// Map avec jusqu'a 10 entrees (methode variadique)
Map<String, Integer> ages = Map.of(
"Alice", 25,
"Bob", 30,
"Charlie", 35
);
// Pour plus de 10 entrees, utilisez Map.ofEntries()
Map<String, String> capitals = Map.ofEntries(
entry("France", "Paris"),
entry("Allemagne", "Berlin"),
entry("Espagne", "Madrid"),
entry("Italie", "Rome"),
entry("Portugal", "Lisbonne")
);
// Acces aux valeurs
System.out.println(ages.get("Alice")); // 25
System.out.println(capitals.get("France")); // Paris
// ATTENTION : Les cles nulles ou dupliquees levent une exception
// Map<String, Integer> invalid = Map.of("key", 1, "key", 2); // ERREUR !
}
}
Conversion entre Collections Mutables et Immutables
Java 10 a introduit les methodes copyOf() pour creer des copies immuables.
import java.util.*;
public class CollectionConversionExample {
public static void main(String[] args) {
// Collection mutable vers immuable (Java 10+)
List<String> mutableList = new ArrayList<>();
mutableList.add("A");
mutableList.add("B");
mutableList.add("C");
// Creer une copie immuable
List<String> immutableCopy = List.copyOf(mutableList);
// La liste originale peut encore etre modifiee
mutableList.add("D");
System.out.println(mutableList); // [A, B, C, D]
System.out.println(immutableCopy); // [A, B, C] - non affectee
// Collection immuable vers mutable
List<String> backToMutable = new ArrayList<>(immutableCopy);
backToMutable.add("E"); // OK !
// Meme principe pour Set et Map
Set<String> mutableSet = new HashSet<>(Set.of("X", "Y", "Z"));
Set<String> immutableSet = Set.copyOf(mutableSet);
Map<String, Integer> mutableMap = new HashMap<>(Map.of("a", 1, "b", 2));
Map<String, Integer> immutableMap = Map.copyOf(mutableMap);
}
}
Multimaps : Plusieurs Valeurs par Cle
Les Multimaps permettent d’associer plusieurs valeurs a une meme cle. Ce pattern est tres utile pour grouper des donnees.
Guava Multimap
Google Guava offre une implementation robuste de Multimap.
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
public class GuavaMultimapExample {
public static void main(String[] args) {
Multimap<String, String> studentCourses = ArrayListMultimap.create();
// Un etudiant peut avoir plusieurs cours
studentCourses.put("Alice", "Mathematiques");
studentCourses.put("Alice", "Physique");
studentCourses.put("Alice", "Informatique");
studentCourses.put("Bob", "Histoire");
studentCourses.put("Bob", "Geographie");
// Recuperer tous les cours d'un etudiant
System.out.println(studentCourses.get("Alice"));
// [Mathematiques, Physique, Informatique]
// Nombre total d'associations
System.out.println(studentCourses.size()); // 5
// Nombre de cles distinctes
System.out.println(studentCourses.keySet().size()); // 2
// Verifier si une association existe
System.out.println(studentCourses.containsEntry("Alice", "Physique")); // true
}
}
Eclipse Collections BiMap
Les BiMaps permettent une recherche bidirectionnelle (cle vers valeur ET valeur vers cle).
import org.eclipse.collections.api.bimap.MutableBiMap;
import org.eclipse.collections.impl.bimap.mutable.HashBiMap;
public class BiMapExample {
public static void main(String[] args) {
MutableBiMap<String, String> translations = new HashBiMap<>();
// Mapping bidirectionnel francais-anglais
translations.put("bonjour", "hello");
translations.put("merci", "thank you");
translations.put("au revoir", "goodbye");
// Recherche normale (cle -> valeur)
System.out.println(translations.get("bonjour")); // hello
// Recherche inverse (valeur -> cle)
System.out.println(translations.inverse().get("hello")); // bonjour
// Les valeurs doivent aussi etre uniques dans une BiMap
// translations.put("salut", "hello"); // Erreur ! "hello" existe deja
}
}
Implementation Manuelle avec Map et List
Si vous ne souhaitez pas ajouter de dependance, vous pouvez implementer un pattern similaire.
import java.util.*;
public class SimpleMultimap<K, V> {
private final Map<K, List<V>> map = new HashMap<>();
public void put(K key, V value) {
map.computeIfAbsent(key, k -> new ArrayList<>()).add(value);
}
public List<V> get(K key) {
return map.getOrDefault(key, Collections.emptyList());
}
public boolean containsKey(K key) {
return map.containsKey(key);
}
public int size() {
return map.values().stream().mapToInt(List::size).sum();
}
public static void main(String[] args) {
SimpleMultimap<String, Integer> scores = new SimpleMultimap<>();
scores.put("Player1", 100);
scores.put("Player1", 150);
scores.put("Player1", 200);
System.out.println(scores.get("Player1")); // [100, 150, 200]
}
}
Bonnes Pratiques
1. Preferez l’immutabilite par defaut
// Mauvais : collection mutable exposee
public class UserService {
private List<User> users = new ArrayList<>();
public List<User> getUsers() {
return users; // Dangereux ! Le client peut modifier la liste
}
}
// Bon : retourner une copie immuable
public class UserService {
private List<User> users = new ArrayList<>();
public List<User> getUsers() {
return List.copyOf(users); // Safe !
}
}
2. Utilisez des collections immuables pour les constantes
public class HttpStatus {
// Constantes immuables - thread-safe et efficaces
public static final Set<Integer> SUCCESS_CODES = Set.of(200, 201, 204);
public static final Set<Integer> ERROR_CODES = Set.of(400, 401, 403, 404, 500);
public static final Map<Integer, String> STATUS_MESSAGES = Map.of(
200, "OK",
201, "Created",
404, "Not Found",
500, "Internal Server Error"
);
}
3. Documentez l’immutabilite dans vos APIs
/**
* Retourne la liste des utilisateurs actifs.
* @return une liste immuable des utilisateurs (ne peut pas etre modifiee)
*/
public List<User> getActiveUsers() {
return List.copyOf(activeUsers);
}
4. Utilisez Collectors.toUnmodifiableList() avec les Streams
import java.util.stream.Collectors;
List<String> filteredNames = names.stream()
.filter(name -> name.startsWith("A"))
.collect(Collectors.toUnmodifiableList()); // Java 10+
Pieges Courants
Piege 1 : Les valeurs null ne sont pas acceptees
// ERREUR : NullPointerException
List<String> list = List.of("a", null, "b"); // NPE !
// Solution : filtrer les nulls ou utiliser une collection mutable
List<String> list = Arrays.asList("a", null, "b"); // Fonctionne (mutable)
Piege 2 : Les doublons dans Set.of()
// ERREUR : IllegalArgumentException
Set<Integer> set = Set.of(1, 2, 2, 3); // Exception !
// Solution : utiliser un HashSet si des doublons sont possibles
Set<Integer> set = new HashSet<>(Arrays.asList(1, 2, 2, 3)); // [1, 2, 3]
Piege 3 : L’immutabilite n’est pas profonde (shallow immutability)
public class Person {
private String name;
// getters et setters...
}
List<Person> people = List.of(new Person("Alice"), new Person("Bob"));
// La liste est immuable, mais les objets Person sont mutables !
people.get(0).setName("Charlie"); // Fonctionne !
// Solution : rendre Person immuable aussi (record Java 16+)
public record Person(String name) {}
Piege 4 : Confusion entre unmodifiable et immutable
List<String> original = new ArrayList<>();
original.add("A");
// Collections.unmodifiableList() cree une VUE, pas une copie
List<String> unmodifiable = Collections.unmodifiableList(original);
original.add("B"); // Modifie aussi unmodifiable !
System.out.println(unmodifiable); // [A, B]
// Solution : utiliser List.copyOf() pour une vraie copie immuable
List<String> trulyImmutable = List.copyOf(original);
original.add("C");
System.out.println(trulyImmutable); // [A, B] - non affecte
Piege 5 : Limites de Map.of()
// Map.of() est limite a 10 paires cle-valeur
// Pour plus, utilisez Map.ofEntries()
Map<String, Integer> largeMap = Map.ofEntries(
entry("k1", 1), entry("k2", 2), entry("k3", 3),
entry("k4", 4), entry("k5", 5), entry("k6", 6),
entry("k7", 7), entry("k8", 8), entry("k9", 9),
entry("k10", 10), entry("k11", 11) // Plus de 10 entrees
);
Conclusion
Les collections immuables en Java sont un outil essentiel pour ecrire du code robuste, thread-safe et maintenable. Voici les points cles a retenir :
| Methode | Usage | Depuis |
|---|---|---|
List.of() | Creer une liste immuable | Java 9 |
Set.of() | Creer un ensemble immuable | Java 9 |
Map.of() | Creer une map immuable (max 10 entrees) | Java 9 |
Map.ofEntries() | Creer une map immuable (illimite) | Java 9 |
List.copyOf() | Copie immuable d’une collection | Java 10 |
Recommandations finales :
- Preferez l’immutabilite sauf si vous avez une raison specifique de modifier la collection
- Utilisez les methodes factory (
List.of(), etc.) pour les petites collections connues a la compilation - Utilisez
copyOf()pour creer des copies immuables de collections existantes - Attention aux nulls : les collections immuables Java ne les acceptent pas
- Considerez Guava ou Eclipse Collections pour des fonctionnalites avancees comme les Multimaps
En maitrisant ces concepts, vous ecrirez du code Java plus sur et plus performant.
Ressources Supplementaires
In-Article Ad
Dev Mode
Tags
Mahmoud DEVO
Senior Full-Stack Developer
I'm a passionate full-stack developer with 10+ years of experience building scalable web applications. I write about Vue.js, Node.js, PostgreSQL, and modern DevOps practices.
Enjoyed this article?
Subscribe to get more tech content delivered to your inbox.
Related Articles
Manipuler les classes Java avec ASM et Javassist : bytecode, instrumentation et fichiers JAR
Apprenez a manipuler les classes Java avec ASM et Javassist : chargement, modification du bytecode, instrumentation et creation de fichiers JAR.
Synchronisation Java avec AtomicInteger : eviter la contention et optimiser les performances
Decouvrez comment utiliser les types atomiques Java (AtomicInteger, AtomicLong, AtomicReference, AtomicBoolean) pour reduire la contention, eviter les blocages et ameliorer les performances.
Les avantages des flux tampons pour une performance optimale
Ameliorez les performances de votre code Java avec les flux tampons ! Decouvrez comment reduire considerablement le nombre d'appels systeme, optimiser l'utilisation des types primitifs, gerer efficacement la journalisation et iterer sur les Maps de maniere performante.