Une liste qui grandit au fil des données, un accès rapide par index et quelques méthodes bien choisies suffisent souvent pour structurer un programme Java proprement. Je montre ici comment fonctionne ArrayList, comment l’utiliser au quotidien, quelles performances attendre et dans quels cas lui préférer une autre collection.
L’essentiel à retenir sur ArrayList
- ArrayList est une liste redimensionnable basée sur un tableau interne.
- Les accès avec get() et set() sont généralement réalisés en temps constant.
- La liste accepte les doublons et les valeurs null.
- Les insertions ou suppressions au milieu coûtent souvent O(n).
- ArrayList n’est pas synchronisée pour les accès concurrents.
Comprendre ArrayList en Java sans complication
ArrayList est l’implémentation la plus courante de l’interface List. Elle conserve les éléments dans un ordre précis, utilise des index qui commencent à 0 et augmente automatiquement sa capacité lorsque la liste devient plus grande.
J’utilise généralement la forme déclarée avec l’interface, car elle laisse davantage de liberté si l’implémentation doit changer plus tard. Le type générique indique clairement le type accepté par la collection.
import java.util.ArrayList;
import java.util.List;
List langages = new ArrayList<>();
langages.add("Java");
langages.add("Python");
langages.add("Go");
System.out.println(langages.get(0)); // Java
Cette collection conserve les doublons et accepte null. Elle convient donc très bien à une série ordonnée de produits, de résultats, de messages ou d’objets métier. En revanche, si l’unicité est obligatoire, je préfère souvent un Set.
Les constructeurs à connaître
List nombres = new ArrayList<>();
List noms = new ArrayList<>(100);
List source = List.of("Alice", "Marc");
List copie = new ArrayList<>(source);
Le constructeur vide crée une liste avec une capacité initiale de 10 éléments. Avec une capacité donnée, comme new ArrayList<>(100), je limite les réallocations lorsque je connais déjà approximativement le volume attendu.
La copie est aussi un point important. List.of() produit une liste non modifiable, tandis que new ArrayList<>(source) crée une liste indépendante et modifiable. Cette différence explique beaucoup d’erreurs dans les petits programmes Java.
Ajouter, lire, modifier et supprimer des éléments
Les opérations de base sont simples, mais leur comportement dépend parfois de l’index ou du type utilisé. Dans mes exemples, je vérifie toujours la taille de la liste avant un accès direct, surtout lorsque les données viennent d’un utilisateur ou d’une API.
List villes = new ArrayList<>();
villes.add("Paris");
villes.add("Lyon");
villes.add(1, "Nantes");
String ville = villes.get(0);
villes.set(0, "Bordeaux");
villes.remove("Nantes");
String derniere = villes.remove(villes.size() - 1);
add(E) ajoute à la fin, alors que add(index, element) insère à une position précise. set() remplace un élément sans modifier la taille. Enfin, remove(int) et remove(Object) n’ont pas le même sens.
Le piège classique avec les entiers
List scores = new ArrayList<>(List.of(10, 20, 30));
scores.remove(1); // supprime l'élément à l'index 1
scores.remove(Integer.valueOf(10)); // supprime la valeur 10
Avec une liste de Integer, remove(1) est interprété comme une suppression par index. Pour supprimer la valeur numérique 1, il faut utiliser Integer.valueOf(1). C’est une subtilité que je préfère traiter explicitement plutôt que de laisser le compilateur décider à la place du lecteur.
Parcourir et filtrer proprement
for (String ville : villes) {
System.out.println(ville);
}
villes.removeIf(ville -> ville.length() < 5);
villes.sort(String.CASE_INSENSITIVE_ORDER);
La boucle améliorée est lisible pour une simple lecture. Pour supprimer selon une condition, removeIf() est plus sûr qu’un appel direct à remove() dans une boucle for-each.
Ce que disent réellement les performances
ArrayList repose sur un tableau interne. Lorsque ce tableau est plein, Java en crée un plus grand et y copie les références existantes. La politique exacte de croissance n’est pas un détail sur lequel je base une optimisation, car elle peut varier selon l’implémentation.
| Opération | Coût habituel | Conséquence pratique |
|---|---|---|
get(index) |
O(1) | Très efficace pour l’accès par position |
set(index, valeur) |
O(1) | Remplacement direct |
add(valeur) à la fin |
O(1) amorti | Rapide sur une longue série d’ajouts |
add(0, valeur) |
O(n) | Les éléments doivent être décalés |
remove(index) |
O(n) | Les éléments suivants sont déplacés |
contains(valeur) |
O(n) | Recherche séquentielle |
Le terme amorti signifie que quelques ajouts peuvent être coûteux lors d’une réallocation, mais que le coût moyen reste constant sur une longue séquence. Pour plusieurs milliers ou millions d’éléments, cela suffit généralement, à condition d’éviter les insertions répétées au début de la liste.
Prévoir la capacité quand le volume est connu
int nombreAttendu = 50_000;
List lignes = new ArrayList<>(nombreAttendu);
for (int i = 0; i < nombreAttendu; i++) {
lignes.add("ligne-" + i);
}
ensureCapacity() peut aussi être utile lorsqu’une liste existe déjà. Je l’emploie seulement lorsque le volume est réellement prévisible, car une capacité surdimensionnée consomme inutilement de la mémoire.
lignes.ensureCapacity(100_000);
lignes.trimToSize();
trimToSize() réduit la capacité interne à la taille actuelle. Ce n’est pas une commande à placer systématiquement après chaque suppression. Elle peut provoquer une nouvelle allocation si la liste doit rapidement grossir à nouveau.
ArrayList ou une autre collection
Le bon choix dépend surtout de la manière dont les données sont utilisées. Je ne choisis pas LinkedList simplement parce qu’une liste subit des insertions, car il faut aussi atteindre la position concernée avant de modifier la chaîne de nœuds.
| Collection | Je la choisis quand | Limite principale |
|---|---|---|
ArrayList |
Je lis souvent par index et j’ajoute surtout à la fin | Insertions au milieu coûteuses |
LinkedList |
J’ai un besoin précis de liste chaînée ou de deque | Accès indexé lent et surcoût mémoire |
HashSet |
Je veux éviter les doublons | Pas d’accès par index |
ArrayDeque |
Je gère une file ou une pile | Ce n’est pas une liste indexée classique |
CopyOnWriteArrayList |
Les lectures sont très nombreuses et les écritures rares | Les écritures copient le tableau |
Pour une file FIFO, où le premier élément entré doit sortir en premier, ArrayDeque est souvent plus cohérent. Pour une liste de résultats affichés et consultés par position, ArrayList reste le choix simple et solide.
J’expose aussi souvent mes méthodes avec le type List plutôt qu’avec ArrayList. Cela réduit le couplage et permet de remplacer l’implémentation sans modifier toute l’API du programme.
Les erreurs qui provoquent les bugs les plus fréquents
Modifier la liste pendant un for-each
for (String nom : noms) {
if (nom.isBlank()) {
noms.remove(nom); // Risque de ConcurrentModificationException
}
}
La solution la plus directe consiste à utiliser removeIf(). Avec un itérateur explicite, sa méthode remove() est également prévue pour modifier la collection pendant son parcours.
noms.removeIf(String::isBlank);
L’exception ConcurrentModificationException sert surtout à signaler un problème de logique. Je ne l’utilise jamais comme mécanisme de synchronisation, car le comportement fail-fast reste une détection de bug au mieux, pas une garantie de sécurité.
Confondre taille et capacité
La taille correspond au nombre d’éléments présents. La capacité désigne l’espace interne disponible avant une nouvelle allocation. Une liste peut donc avoir une taille de 3 et une capacité supérieure à 3, sans que les cases restantes soient accessibles avec get().
Oublier les index valides
Pour lire ou remplacer un élément, l’index doit être compris entre 0 et size() - 1. En revanche, une insertion peut utiliser l’index égal à size(), car cela revient à ajouter l’élément à la fin.
if (!noms.isEmpty()) {
String premier = noms.get(0);
}
Lire aussi : Java - Maîtriser indexOf() : Guide Complet et Astuces
Penser qu’une sous-liste est une copie
List extrait = noms.subList(0, 2);
subList() renvoie une vue de la liste originale, et non une copie indépendante. Pour obtenir une nouvelle liste modifiable, j’écris plutôt new ArrayList<>(noms.subList(0, 2)). Cette précaution évite des effets de bord difficiles à repérer.
Gérer la concurrence et les données immuables
ArrayList n’est pas synchronisée. Si plusieurs threads accèdent à la même instance et qu’au moins l’un d’eux la modifie structurellement, il faut une protection externe ou une collection conçue pour ce scénario.
List partagée =
java.util.Collections.synchronizedList(new ArrayList<>());
synchronized (partagée) {
for (String élément : partagée) {
System.out.println(élément);
}
}
Le bloc synchronized autour de l’itération est important, même lorsque la liste a été enveloppée avec synchronizedList(). Pour des lectures très fréquentes et des modifications exceptionnelles, CopyOnWriteArrayList peut être pertinente, mais son coût d’écriture la rend inadaptée à une liste qui change souvent.
Dans le code moderne, je distingue aussi clairement les listes modifiables et immuables. Une liste immuable réduit les effets de bord, tandis qu’une ArrayList est préférable dès qu’un traitement doit ajouter, retirer ou réordonner des éléments.
Le bon réflexe pour choisir et utiliser ArrayList
Je pars d’une règle simple. Si la collection est ordonnée, consultée par index et alimentée principalement à la fin, ArrayList est probablement le meilleur point de départ. Je déclare ensuite la variable avec List, je précise le type générique et je réserve une capacité initiale seulement si le volume est connu.
Si le programme insère constamment au début, cherche l’unicité, traite une file ou partage les données entre plusieurs threads, une autre collection sera peut-être plus adaptée. Le plus important n’est pas de mémoriser toutes les classes du framework, mais de relier le choix de la structure aux opérations réellement effectuées.
Avec ces repères, ArrayList devient une solution prévisible, lisible et performante pour la grande majorité des listes applicatives en Java.