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.

☕ Java 21 LTS🧠 Schémas en mémoire📊 Tableau des complexités✅ Pièges et bonnes pratiques⏱️ 25 min

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.

Hiérarchie des collections Java : Collection, List, Set, Queue et Map
La famille des collections Java : trois branches sous Collection (List, Set, Queue) et une famille à part, Map.
🎯
Ce que tu sauras faire à la fin

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

InterfaceCe qu’elle représenteDoublons ?Ordre ?Implémentation par défaut
ListUne séquence d’éléments, accessible par index✅ Oui✅ Ordre d’insertionArrayList
SetUn ensemble d’éléments uniques❌ NonSelon l’implémentationHashSet
Queue / DequeUne file (FIFO) ou une pile (LIFO)✅ OuiOrdre de traitementArrayDeque
MapDes paires clé → valeurClés uniquesSelon l’implémentationHashMap
✍️
Bonne pratique : programme vers l’interface

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 vs LinkedList en Java : tableau contigu contre nœuds chaînés en mémoire
ArrayList et LinkedList, deux collections Java aux structures très différentes 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);
}
ArrayList

LinkedList : 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.

⚠️
En pratique, choisis ArrayList

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ésent
HashSet, LinkedHashSet, TreeSet
ImplémentationOrdrecontains / add / removeQuand l’utiliser
HashSetAucun garantiO(1)Par défaut : unicité et recherche rapide
LinkedHashSetOrdre d’insertionO(1)Dédoublonner en gardant l’ordre d’origine
TreeSetTrié (ordre naturel ou Comparator)O(log n)Garder un ensemble trié, chercher le plus petit, le suivant, une plage
🚀
Remplacer list.contains() par un Set

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");
}
HashMap

Comment fonctionne une HashMap ?

Fonctionnement d'une HashMap Java : hashCode, buckets et collisions
Une HashMap, la collection Java clé → valeur, range chaque couple dans un bucket calculé à partir du hashCode.

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

⚠️
La règle d’or : equals() et hashCode() vont ensemble

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émentationOrdre des clésget / putQuand l’utiliser
HashMapAucun garantiO(1)Par défaut
LinkedHashMapOrdre d’insertion (ou d’accès)O(1)Garder l’ordre, ou faire un cache LRU
TreeMapClés triéesO(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 computeIfAbsent

Queue 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()); // 1
ArrayDeque et PriorityQueue
📌
Oublie la classe Stack

La 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.

CollectionAjoutAccès / rechercheSuppressionRemarque
ArrayListO(1)* en fin · O(n) au milieuget(i) O(1) · contains O(n)O(n)* amorti (redimensionnement occasionnel)
LinkedListO(1) aux extrémitésget(i) O(n) · contains O(n)O(1) aux extrémitésBeaucoup de mémoire par élément
HashSetO(1)contains O(1)O(1)Ordre non garanti
LinkedHashSetO(1)O(1)O(1)Garde l’ordre d’insertion
TreeSetO(log n)O(log n)O(log n)Toujours trié
HashMapO(1)get O(1)O(1)Dépend d’un bon hashCode()
TreeMapO(log n)O(log n)O(log n)Clés triées, recherche par plage
ArrayDequeO(1) aux deux boutspeek O(1)O(1) aux deux boutsPile et file idéales
PriorityQueueO(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 ?

Arbre de décision pour choisir une collection Java : HashMap, HashSet, ArrayList, ArrayDeque
Choisir une collection Java en une image : pars de ton besoin, pas de la structure.
  1. Tu associes une clé à une valeur ? → HashMap (triée : TreeMap, ordre d’insertion : LinkedHashMap).
  2. Tu veux des éléments uniques ou tester l’appartenance rapidement ? → HashSet (trié : TreeSet).
  3. Tu traites des éléments dans l’ordre d’arrivée ou en pile ? → ArrayDeque (par priorité : PriorityQueue).
  4. 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 parcours

Piè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}
Streams

Java 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.

Comments

No comments yet. Why don’t you start the discussion?

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *