ArrayList en Java - méthodes, performances et pièges

Denis Ribeiro .

13 septembre 2026

Graphique comparant le temps d'exécution de ArrayList et LinkedList. Pour 1 million d'éléments, ArrayList est plus rapide.

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.

Questions fréquentes

Le constructeur vide utilise une capacité initiale de 10 éléments. Si le volume attendu est connu, une capacité comme new ArrayList<>(100) ou new ArrayList<>(50000) limite les réallocations. Il faut toutefois éviter de surdimensionner la liste, car cela consomme inutilement de la mémoire.
Dans une List, remove(1) appelle la version qui supprime l’élément situé à l’index 1. Pour supprimer la valeur numérique 1, utilisez remove(Integer.valueOf(1)).
ArrayList convient lorsque les accès par index sont fréquents et que les ajouts se font principalement à la fin. Les insertions ou suppressions au début ou au milieu coûtent généralement O(n). Pour gérer une file ou une pile, ArrayDeque est souvent plus cohérente, tandis que LinkedList se justifie surtout pour un besoin précis de liste chaînée ou de deque.
Évitez d’appeler remove() directement dans une boucle for-each, car cela peut provoquer une ConcurrentModificationException. Utilisez plutôt removeIf(), par exemple noms.removeIf(String::isBlank), ou la méthode remove() d’un itérateur explicite.
Évaluer l'article

Moyenne: 0.0 / 5 · 0 évaluations

Tags

arraylist list performances concurrence capacité
Autor Denis Ribeiro
Denis Ribeiro
Je m'appelle Denis Ribeiro et j'ai quatre ans d'expérience dans le domaine de la technologie, en particulier dans les domaines du web, de l'intelligence artificielle, des réseaux et de la sécurité. Mon intérêt pour ces sujets a commencé dès mon plus jeune âge, lorsque j'ai découvert à quel point la technologie pouvait transformer notre quotidien. J'aime expliquer des concepts complexes de manière simple, afin d'aider mes lecteurs à mieux comprendre les enjeux actuels et futurs de notre monde numérique. Dans mes écrits, je m'efforce de fournir des informations utiles, précises et à jour. Je prends le temps de vérifier mes sources et de comparer les informations, tout en suivant les dernières tendances du secteur. Mon objectif est de rendre l'apprentissage accessible à tous, en organisant mes connaissances de manière claire et structurée, afin que chacun puisse naviguer avec confiance dans cet univers en constante évolution.
Commentaires (0)
Ajouter un commentaire