Une liste de tâches, des résultats d’API ou des produits à afficher dans l’ordre ont un point commun : les éléments doivent rester ordonnés et facilement accessibles. En Java, l’interface List répond à ce besoin avec un ensemble de méthodes cohérent, mais le choix de son implémentation influence directement les performances, la mutabilité et la sécurité du code. Je présente ici les différences entre ArrayList, LinkedList et les autres options utiles, avec des exemples directement réutilisables.
Les choix essentiels pour utiliser une liste Java sans mauvaise surprise
-
Listconserve l’ordre, accepte généralement les doublons et utilise des index commençant à 0. -
ArrayListest le choix par défaut pour la plupart des applications. -
LinkedListdevient intéressante surtout pour les ajouts et suppressions fréquents aux extrémités. -
List.of()etList.copyOf()créent des listes non modifiables. -
Les listes ne sont pas automatiquement thread-safe et certaines opérations peuvent lever
UnsupportedOperationException.
Ce que garantit vraiment l’interface List
List est une interface, pas une structure de données concrète. Elle décrit le comportement attendu d’une collection ordonnée : accès par position, recherche d’un élément, insertion à un index donné et conservation de l’ordre d’ajout.
Les index commencent à 0. Le premier élément se lit donc avec get(0), tandis que le dernier se trouve à get(size() - 1). Une liste peut généralement contenir plusieurs fois la même valeur, contrairement à un Set qui sert plutôt à garantir l’unicité.
List villes = new ArrayList<>();
villes.add("Paris");
villes.add("Lyon");
villes.add(1, "Nantes");
String premiereVille = villes.get(0);
Dans cet exemple, Nantes est insérée à l’index 1, ce qui décale Lyon vers la position suivante. C’est précisément cette maîtrise de la position qui rend List adaptée aux résultats paginés, aux étapes d’un processus ou à une séquence d’événements.
La bonne pratique consiste à déclarer la variable avec le type de l’interface et à choisir l’implémentation uniquement lors de la création. Je garde ainsi la liberté de remplacer ArrayList par une autre solution sans modifier les méthodes qui utilisent la collection.
List utilisateurs = new ArrayList<>();
ArrayList ou LinkedList pour quel usage
Les deux implémentations générales les plus connues sont ArrayList et LinkedList. Elles offrent toutes deux les opérations de List, mais leur organisation interne n’est pas la même, ce qui change le coût de certaines opérations.
| Critère | ArrayList | LinkedList |
|---|---|---|
| Accès par index | Très rapide, généralement en temps constant | Plus lent, car la liste doit parcourir les nœuds |
| Ajout en fin | Rapide en moyenne | Rapide |
| Insertion au milieu | Les éléments doivent être décalés | Le déplacement jusqu’au nœud reste coûteux |
| Suppression aux extrémités | Correcte en fin, moins adaptée au début | Très adaptée au début et à la fin |
| Usage mémoire | Plus compact | Plus élevé à cause des liens entre nœuds |
ArrayList est mon choix par défaut. Elle exploite un tableau redimensionnable, offre un accès direct efficace et fonctionne très bien quand le programme lit souvent les éléments ou parcourt la collection de bout en bout.
LinkedList a du sens lorsque le comportement ressemble davantage à celui d’une file ou d’une deque, avec des ajouts et suppressions fréquents au début ou à la fin. Elle n’est pas automatiquement plus rapide pour une insertion au milieu : trouver la position peut déjà demander un parcours coûteux.
Une erreur fréquente consiste à choisir LinkedList dès qu’une insertion est prévue. Dans une application classique, la fréquence des lectures et la taille réelle de la collection comptent souvent davantage que l’opération théorique d’insertion.
Créer une liste modifiable, fixe ou non modifiable
Le mot « liste » ne suffit pas à déterminer si l’on peut ajouter ou supprimer des éléments. Les méthodes de modification sont parfois optionnelles, et certaines fabriques renvoient volontairement une collection protégée.
Une liste modifiable
List langages = new ArrayList<>();
langages.add("Java");
langages.add("Kotlin");
langages.remove("Kotlin");
Cette forme convient lorsqu’une collection doit évoluer pendant l’exécution. Pour initialiser une liste à partir d’autres valeurs tout en conservant la possibilité de la modifier, je préfère créer explicitement une copie.
List sources = List.of("API", "Base de données");
List copie = new ArrayList<>(sources);
copie.add("Cache");
Les fabriques pratiques de Java
List.of() crée une liste non modifiable. Elle refuse également les valeurs null. C’est une excellente solution pour transmettre une configuration constante ou retourner quelques valeurs sans exposer une collection modifiable.
List codes = List.of(200, 201, 204);
// codes.add(500); déclenche UnsupportedOperationException
List.copyOf() produit aussi une liste non modifiable à partir d’une collection existante. À l’inverse, Arrays.asList() crée une liste de taille fixe liée au tableau d’origine : set() est possible, mais add() et remove() ne le sont pas.
String[] tableau = {"A", "B"};
List tailleFixe = Arrays.asList(tableau);
tailleFixe.set(0, "X");
// tailleFixe.add("C") provoquerait une exception
Cette distinction évite beaucoup de bugs. Avant de modifier une liste reçue en paramètre, je vérifie toujours si le contrat de la méthode garantit réellement qu’elle est mutable.
Les opérations utiles au quotidien
Une liste devient intéressante lorsqu’elle simplifie le traitement des données. Les méthodes contains, indexOf, removeIf, sort et subList couvrent une grande partie des besoins courants.
List notes = new ArrayList<>(List.of(12, 8, 17, 9));
notes.removeIf(note -> note < 10);
notes.sort(Comparator.reverseOrder());
for (Integer note : notes) {
System.out.println(note);
}
Pour parcourir les éléments, la boucle améliorée ou la méthode forEach sont généralement plus lisibles qu’un accès répété par index. Ce point est particulièrement important avec LinkedList, car une succession de get(i) peut entraîner un parcours inutile de la structure.
notes.forEach(note -> System.out.println("Note : " + note));
subList(from, to) renvoie une vue sur la liste d’origine, et non une copie indépendante. Une modification de la sous-liste peut donc modifier la collection principale. Pour obtenir une copie sûre, je l’encapsule dans un nouveau constructeur.
List extrait = new ArrayList<>(notes.subList(0, 2));
Depuis Java 21, les listes disposent aussi d’opérations liées à l’ordre séquentiel comme getFirst(), getLast() et reversed(). Cette dernière renvoie une vue inversée, pas nécessairement une nouvelle liste, ce qui mérite de la prudence si la collection est ensuite modifiée.
Les erreurs qui reviennent le plus souvent
Confondre suppression par index et suppression par valeur
Avec une liste d’entiers, remove(1) supprime l’élément situé à l’index 1, et non la valeur 1. Pour supprimer la valeur, il faut fournir un objet Integer.
List nombres = new ArrayList<>(List.of(10, 20, 30));
nombres.remove(1); // supprime 20
nombres.remove(Integer.valueOf(30)); // supprime la valeur 30
C’est un détail de surcharge très discret, mais il peut produire des résultats incorrects sans provoquer d’erreur de compilation. Je recommande de rendre l’intention explicite dès que le type de la liste est numérique.
Modifier une liste pendant son parcours
Supprimer directement un élément dans une boucle for-each peut déclencher une ConcurrentModificationException. La méthode removeIf est souvent plus claire et gère précisément ce cas.
utilisateurs.removeIf(nom -> nom.isBlank());
Si un traitement plus complexe est nécessaire, un Iterator permet une suppression contrôlée. Dans tous les cas, il faut distinguer une modification par le même thread pendant l’itération d’un vrai problème de concurrence entre plusieurs threads.
Lire aussi : Java Vector - Quand l'utiliser (ou l'éviter) en 2024 ?
Oublier la sécurité des threads
ArrayList et LinkedList ne sont pas thread-safe. Pour plusieurs lectures et très peu d’écritures, CopyOnWriteArrayList peut être pertinente, car les itérations travaillent sur une copie stable, mais chaque modification coûte davantage.
Collections.synchronizedList() fournit une enveloppe synchronisée, mais l’itération doit aussi être protégée par un bloc synchronized. Pour ma part, je ne choisis une collection concurrente qu’après avoir identifié le partage réel entre threads, car la synchronisation ajoute un coût qui n’apporte rien dans un code strictement local.
Le réflexe simple pour choisir la bonne collection
Dans la majorité des projets, je commence avec List et ArrayList<>. Cette combinaison offre une API claire, un accès rapide par index et un comportement prévisible pour les parcours, les tris et les recherches.
Je passe à LinkedList seulement si le profil d’utilisation justifie réellement des opérations fréquentes aux deux extrémités. Pour des valeurs constantes, List.of ou List.copyOf rendent l’intention plus sûre et évitent qu’une méthode modifie accidentellement des données partagées.
Le meilleur choix ne dépend donc pas du nom de la classe, mais de trois questions concrètes : la liste doit-elle être modifiable, l’accès par index est-il fréquent et plusieurs threads l’utilisent-ils ? Une fois ces réponses établies, l’implémentation appropriée devient généralement évidente.