ArrayList ou LinkedList ? HashMap ou TreeMap ? HashSet ou List ? Choisir la mauvaise structure peut rendre un programme Java 100 fois plus lent, ou introduire des bugs sournois. Dans ce guide, tu vas comprendre les collections Java de l’intérieur, avec des schémas, un tableau des complexités et une méthode simple pour savoir quand utiliser quoi.
Le Java Collections Framework est la boîte à outils que tout développeur Java utilise chaque jour : des listes, des ensembles, des files et des dictionnaires (maps), prêts à l’emploi dans le package java.util. Le problème n’est pas de savoir s’en servir, c’est de choisir la bonne. Car toutes stockent des éléments, mais aucune ne le fait de la même façon.

Choisir entre List, Set, Map et Queue en quelques secondes, comprendre pourquoi ArrayList est presque toujours préférable à LinkedList, expliquer comment fonctionne une HashMap, lire un tableau de complexités, et éviter les pièges classiques (ConcurrentModificationException, equals/hashCode, listes immuables…).
Les 4 familles de collections Java
| Interface | Ce qu’elle représente | Doublons ? | Ordre ? | Implémentation par défaut |
|---|---|---|---|---|
List | Une séquence d’éléments, accessible par index | ✅ Oui | ✅ Ordre d’insertion | ArrayList |
Set | Un ensemble d’éléments uniques | ❌ Non | Selon l’implémentation | HashSet |
Queue / Deque | Une file (FIFO) ou une pile (LIFO) | ✅ Oui | Ordre de traitement | ArrayDeque |
Map | Des paires clé → valeur | Clés uniques | Selon l’implémentation | HashMap |
Déclare tes variables avec le type de l’interface et choisis l’implémentation à droite : List<String> noms = new ArrayList<>();. Tu pourras changer d’implémentation plus tard sans toucher au reste du code.
List : la collection Java ArrayList ou LinkedList ?
Une List garde les éléments dans l’ordre où tu les ajoutes, accepte les doublons et permet l’accès par index (get(0)). Deux implémentations principales, qui fonctionnent de façon très différente en mémoire :

ArrayList : un tableau qui s’agrandit
ArrayList repose sur un tableau interne. Quand il est plein, Java en crée un plus grand (environ 1,5 fois la taille) et copie les éléments. Résultat : l’accès par index est instantané, et l’ajout en fin de liste est très rapide en moyenne.
import java.util.ArrayList;
import java.util.List;
List<String> courses = new ArrayList<>();
courses.add("Pain");
courses.add("Fromage");
courses.add("Tomates");
courses.add(1, "Café"); // insère à l'index 1 (décale la suite)
System.out.println(courses.get(0)); // Pain → O(1)
System.out.println(courses.contains("Café")); // true → O(n)
courses.remove("Fromage"); // → O(n)
for (String article : courses) {
System.out.println(article);
}ArrayListLinkedList : une chaîne de nœuds
LinkedList est une liste doublement chaînée : chaque élément est un objet « nœud » qui pointe vers le précédent et le suivant. Ajouter ou retirer un élément aux extrémités est immédiat, mais accéder au 500 000ᵉ élément oblige à parcourir la chaîne.
Sur les processeurs modernes, un tableau contigu profite du cache CPU, alors que les nœuds d’une LinkedList sont éparpillés en mémoire et coûtent chacun environ 24 octets de plus. Même pour des insertions au milieu, ArrayList est souvent plus rapide. Pour une file ou une pile, préfère ArrayDeque. LinkedList n’est presque jamais le bon choix.
Set : la collection Java des éléments uniques
Un Set refuse les doublons : ajouter deux fois le même élément n’a aucun effet. C’est la structure idéale pour dédoublonner une liste ou tester rapidement l’appartenance (« cet e-mail est-il déjà inscrit ? »).
import java.util.*;
List<String> villes = List.of("Liège", "Namur", "Liège", "Mons", "Bruxelles", "Namur");
Set<String> hash = new HashSet<>(villes); // ordre imprévisible
Set<String> lies = new LinkedHashSet<>(villes); // ordre d'insertion
Set<String> triees = new TreeSet<>(villes); // ordre alphabétique
System.out.println(lies); // [Liège, Namur, Mons, Bruxelles]
System.out.println(triees); // [Bruxelles, Liège, Mons, Namur]
System.out.println(hash.contains("Mons")); // true, en O(1)
System.out.println(hash.add("Mons")); // false : déjà présentHashSet, LinkedHashSet, TreeSet| Implémentation | Ordre | contains / add / remove | Quand l’utiliser |
|---|---|---|---|
HashSet | Aucun garanti | O(1) | Par défaut : unicité et recherche rapide |
LinkedHashSet | Ordre d’insertion | O(1) | Dédoublonner en gardant l’ordre d’origine |
TreeSet | Trié (ordre naturel ou Comparator) | O(log n) | Garder un ensemble trié, chercher le plus petit, le suivant, une plage |
Dans une boucle, list.contains(x) parcourt toute la liste à chaque fois : avec 100 000 éléments, c’est 100 000 comparaisons par appel. Copie d’abord la liste dans un HashSet : chaque contains devient quasi instantané.
Map : la collection Java clé → valeur
Une Map associe des clés uniques à des valeurs, comme un dictionnaire : un identifiant à un utilisateur, un mot à son nombre d’occurrences, un code postal à une ville. Elle ne fait pas partie de l’interface Collection, mais elle appartient bien au framework.
import java.util.HashMap;
import java.util.Map;
Map<String, Integer> ages = new HashMap<>();
ages.put("Alice", 31);
ages.put("Bruno", 27);
ages.put("Alice", 32); // remplace la valeur
System.out.println(ages.get("Alice")); // 32
System.out.println(ages.get("Zoé")); // null
System.out.println(ages.getOrDefault("Zoé", 0)); // 0
for (Map.Entry<String, Integer> e : ages.entrySet()) {
System.out.println(e.getKey() + " a " + e.getValue() + " ans");
}HashMapComment fonctionne une HashMap ?

La HashMap est un tableau de « tiroirs » (buckets). Pour ranger une clé, Java calcule son hashCode(), en déduit le numéro du tiroir, et y dépose le couple. Pour lire, il refait le même calcul et va directement au bon tiroir : c’est pour ça que get() et put() sont en O(1) en moyenne.
Quand deux clés tombent dans le même tiroir (collision), Java les compare avec equals(). Depuis Java 8, un tiroir trop rempli (plus de 8 éléments) se transforme en arbre équilibré, ce qui limite le pire cas à O(log n).
Si tu utilises tes propres objets comme clés de HashMap ou dans un HashSet, redéfinis les deux méthodes, de façon cohérente. Sinon deux objets « égaux » finiront dans des tiroirs différents et ton Set contiendra des doublons. Les record Java les génèrent automatiquement.
// equals(), hashCode() et toString() sont générés automatiquement
record Point(int x, int y) {}
Map<Point, String> carte = new HashMap<>();
carte.put(new Point(2, 3), "Trésor");
System.out.println(carte.get(new Point(2, 3))); // Trésor ✅Un record comme clé de Map| Implémentation | Ordre des clés | get / put | Quand l’utiliser |
|---|---|---|---|
HashMap | Aucun garanti | O(1) | Par défaut |
LinkedHashMap | Ordre d’insertion (ou d’accès) | O(1) | Garder l’ordre, ou faire un cache LRU |
TreeMap | Clés triées | O(log n) | Parcourir les clés dans l’ordre, chercher une plage (dates, scores…) |
Deux méthodes très pratiques de Java 8+ évitent bien des if :
// Compter les mots d'un texte
Map<String, Integer> compte = new HashMap<>();
for (String mot : "le chat et le chien et le lapin".split(" ")) {
compte.merge(mot, 1, Integer::sum);
}
System.out.println(compte); // le=3, et=2, chat=1… (ordre non garanti)
// Regrouper des élèves par classe
Map<String, List<String>> classes = new HashMap<>();
classes.computeIfAbsent("6A", k -> new ArrayList<>()).add("Alice");
classes.computeIfAbsent("6A", k -> new ArrayList<>()).add("Bruno");merge et computeIfAbsentQueue et Deque : les collections Java pour files et piles
Une Queue traite les éléments dans un ordre précis : le premier arrivé est le premier servi (FIFO), comme une file d’attente. Une Deque (« double-ended queue ») permet d’ajouter et retirer aux deux bouts : elle sert donc aussi de pile (LIFO).
import java.util.*;
// File d'attente (FIFO)
Deque<String> file = new ArrayDeque<>();
file.offer("Client 1");
file.offer("Client 2");
System.out.println(file.poll()); // Client 1
// Pile (LIFO) : historique « Annuler »
Deque<String> annuler = new ArrayDeque<>();
annuler.push("Écrire titre");
annuler.push("Mettre en gras");
System.out.println(annuler.pop()); // Mettre en gras
// File de priorité : la plus petite valeur sort en premier
Queue<Integer> urgences = new PriorityQueue<>();
urgences.addAll(List.of(5, 1, 3));
System.out.println(urgences.poll()); // 1ArrayDeque et PriorityQueueLa vieille classe Stack (héritée de Vector) est synchronisée et lente. La documentation officielle recommande ArrayDeque pour les piles comme pour les files.
Le tableau des complexités des collections Java
La complexité (notation « grand O ») indique comment le temps d’une opération évolue quand la collection grossit. O(1) : temps constant, quelle que soit la taille. O(log n) : augmente très lentement. O(n) : proportionnel au nombre d’éléments.
| Collection | Ajout | Accès / recherche | Suppression | Remarque |
|---|---|---|---|---|
ArrayList | O(1)* en fin · O(n) au milieu | get(i) O(1) · contains O(n) | O(n) | * amorti (redimensionnement occasionnel) |
LinkedList | O(1) aux extrémités | get(i) O(n) · contains O(n) | O(1) aux extrémités | Beaucoup de mémoire par élément |
HashSet | O(1) | contains O(1) | O(1) | Ordre non garanti |
LinkedHashSet | O(1) | O(1) | O(1) | Garde l’ordre d’insertion |
TreeSet | O(log n) | O(log n) | O(log n) | Toujours trié |
HashMap | O(1) | get O(1) | O(1) | Dépend d’un bon hashCode() |
TreeMap | O(log n) | O(log n) | O(log n) | Clés triées, recherche par plage |
ArrayDeque | O(1) aux deux bouts | peek O(1) | O(1) aux deux bouts | Pile et file idéales |
PriorityQueue | O(log n) | peek O(1) | poll O(log n) | Tas binaire |
Concrètement, sur 1 million d’éléments, une recherche O(n) peut demander 1 million de comparaisons, une recherche O(log n) une vingtaine, et O(1) une seule. C’est exactement la même logique que les index d’une base MySQL : la bonne structure évite de tout parcourir.
Quelle collection Java choisir ?

- Tu associes une clé à une valeur ? →
HashMap(triée :TreeMap, ordre d’insertion :LinkedHashMap). - Tu veux des éléments uniques ou tester l’appartenance rapidement ? →
HashSet(trié :TreeSet). - Tu traites des éléments dans l’ordre d’arrivée ou en pile ? →
ArrayDeque(par priorité :PriorityQueue). - Sinon, une liste ordonnée avec accès par index →
ArrayList.
Les pièges classiques avec les collections Java
Piège 1 : ConcurrentModificationException
Supprimer un élément pendant une boucle for-each lève une ConcurrentModificationException. Utilise removeIf ou un Iterator :
List<Integer> nombres = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6));
// ❌ ConcurrentModificationException
for (Integer n : nombres) {
if (n % 2 == 0) nombres.remove(n);
}
// ✅ Java 8+
nombres.removeIf(n -> n % 2 == 0);
// ✅ Avec un Iterator
Iterator<Integer> it = nombres.iterator();
while (it.hasNext()) {
if (it.next() > 3) it.remove();
}Supprimer pendant un parcoursPiège 2 : les collections immuables
List.of(), Set.of() et Map.of() (Java 9+) créent des collections non modifiables : tout add lève une UnsupportedOperationException. Elles refusent aussi les valeurs null. Arrays.asList(), lui, renvoie une liste de taille fixe. Pour une liste modifiable : new ArrayList<>(List.of(...)).
Piège 3 : remove(int) ou remove(Object) ?
List<Integer> liste = new ArrayList<>(List.of(10, 20, 30));
liste.remove(1); // retire l'élément À L'INDEX 1 → [10, 30]
liste.remove(Integer.valueOf(10)); // retire la VALEUR 10 → [30]remove sur une List<Integer>Piège 4 : ignorer la capacité initiale
Si tu sais que tu vas stocker 100 000 éléments, indique-le : new ArrayList<>(100_000) ou new HashMap<>(150_000). Tu évites des dizaines de redimensionnements et de copies.
Piège 5 : les collections et le multithreading
ArrayList et HashMap ne sont pas sûres en multithread. Entre plusieurs threads, utilise ConcurrentHashMap, CopyOnWriteArrayList (beaucoup de lectures, peu d’écritures) ou une BlockingQueue pour un modèle producteur/consommateur.
Bonus : collections Java et Streams
Depuis Java 8, l’API Stream permet de filtrer, transformer et regrouper une collection de façon déclarative :
record Eleve(String nom, String classe, double moyenne) {}
List<Eleve> eleves = List.of(
new Eleve("Alice", "6A", 15.5),
new Eleve("Bruno", "6B", 11.0),
new Eleve("Chloé", "6A", 17.0)
);
// Noms des élèves au-dessus de 12, triés
List<String> reussite = eleves.stream()
.filter(e -> e.moyenne() >= 12)
.map(Eleve::nom)
.sorted()
.toList(); // Java 16+ : liste immuable
// Moyenne par classe
Map<String, Double> parClasse = eleves.stream()
.collect(Collectors.groupingBy(Eleve::classe,
Collectors.averagingDouble(Eleve::moyenne)));
// {6A=16.25, 6B=11.0}StreamsJava 21 a aussi ajouté les Sequenced Collections : getFirst(), getLast() et reversed() sont désormais disponibles sur List, Deque, LinkedHashSet et LinkedHashMap. Fini liste.get(liste.size() - 1).
Pour mettre tout ça en pratique sur un vrai projet, découvre notre tutoriel logiciel de gestion de restaurant avec JavaFX et SQLite, qui utilise listes, maps et streams à chaque écran.
FAQ : collections Java
Quelle est la différence entre ArrayList et LinkedList ?
ArrayList stocke les éléments dans un tableau : l’accès par index est en O(1). LinkedList chaîne des nœuds : l’ajout aux extrémités est en O(1) mais l’accès par index en O(n). En pratique, ArrayList est plus rapide dans la grande majorité des cas.
Quelle est la différence entre HashMap et TreeMap ?
HashMap offre des opérations en O(1) en moyenne mais ne garantit aucun ordre. TreeMap garde les clés triées et permet des recherches par plage, avec des opérations en O(log n).
Quand utiliser un HashSet plutôt qu’une List ?
Quand les éléments doivent être uniques ou quand tu dois tester souvent si un élément est présent : contains est en O(1) sur un HashSet contre O(n) sur une List.
Pourquoi redéfinir equals et hashCode ?
Parce que HashMap et HashSet utilisent hashCode pour trouver le bon emplacement et equals pour comparer les objets. Si les deux ne sont pas cohérents, des objets égaux ne seront pas reconnus comme tels. Les records Java les génèrent automatiquement.
Les collections Java sont-elles thread-safe ?
Non pour ArrayList, HashMap ou HashSet. En environnement multithread, utilise les collections du package java.util.concurrent comme ConcurrentHashMap, CopyOnWriteArrayList ou les BlockingQueue.
Conclusion
Les collections Java se choisissent à partir du besoin : une clé → valeur, c’est une Map ; des éléments uniques, un Set ; un ordre de traitement, une Deque ; et pour tout le reste, une ArrayList. Ajoute à cela le réflexe equals/hashCode, removeIf pour supprimer en boucle et les collections concurrentes en multithread, et tu éviteras 95 % des problèmes.
Pour approfondir, la documentation Oracle du Collections Framework détaille chaque interface. Et si tu veux comparer avec d’autres langages, les tableaux de notre guide des jointures SQL montrent la même logique de « relier des données » côté base de données.

