Les listes en Java

 

Dans cet article (partie 2 de la série sur les collections), nous allons nous concentrer sur la famille List du Framework Collections : ses caractéristiques, ses principales implémentations (ArrayList, LinkedList…), leurs performances et les bonnes pratiques d’utilisation au quotidien.

  1. Introduction aux collections Java
  2. Les listes en Java (vous êtes ici)
  3. Les ensembles (Set) en Java
  4. Les files (Queue) et Deques en Java
  5. Collections Map (article à venir)
  6. Utilisations avancées des collections (article à venir)

Qu’est‑ce qu’une List ?

List est une sous‑interface de Collection qui représente une séquence ordonnée d’éléments :

  • Les éléments ont un ordre (position/index).
  • Les doublons sont autorisés.
  • L’accès indexé est possible (méthodes get, set, add(int, E), remove(int), etc.).

La signature :

public interface List<E> extends Collection<E> { /* … */ }

En plus des méthodes héritées de Collection, List ajoute entre autres :

Méthode Description
E get(int index) Retourne l’élément à l’index donné.
E set(int index, E element) Remplace l’élément à l’index par element et retourne l’ancien.
void add(int index, E element) Insère element à la position index (décale les éléments suivants).
E remove(int index) Supprime et retourne l’élément à la position index.
int indexOf(Object o) Retourne le premier index de o, ou -1 s’il est absent.
int lastIndexOf(Object o) Retourne le dernier index de o, ou -1 s’il est absent.
ListIterator listIterator() Itérateur bidirectionnel sur la liste.
ListIterator listIterator(int index) Itérateur bidirectionnel démarrant à index.
List subList(int fromIndex, int toIndex) retourne une vue d’une portion [fromIndex, toIndex) de la liste (liée à l’originale).
void replaceAll(UnaryOperator operator) Remplace chaque élément par operator.apply(élément).
void sort(Comparator<? super E> c) Trie en place selon le comparateur.

Principales implémentations

ArrayList

  • Structure interne : tableau redimensionnable.
  • Accès par index très rapide (en O(1)), insertion/suppression en fin rapides, mais insertion/suppression au milieu coûteuses (décalage des éléments).
  • Mémoire compacte, très utilisée par défaut.

Cas d’usage : lecture fréquente par index, ajout en fin, listes majoritairement immuables après construction.

LinkedList

  • Liste doublement chaînée (chaque élément connaît son précédent et son suivant).
  • Insertion/suppression au début/au milieu rapides si vous disposez déjà d’un ListIterator positionné, mais accès par index en O(n).
  • Surcoût mémoire (pointeurs) + moins bonne avec le cache CPU.

Cas d’usage : scénarios spécialisés où l’on enlève/insère souvent au milieu via un itérateur, ou quand on utilise l’API Deque que LinkedList implémente aussi (pile/queue simple).

Autres options utiles

  • CopyOnWriteArrayList : thread‑safe, excellente en lecture majoritaire (chaque écriture copie la liste). À éviter si beaucoup d’écritures.
  • Collections.synchronizedList(new ArrayList<>()) : wrapper synchronisé basique.
  • Listes immuables : List.of(...) (Java 9+) ou Collections.unmodifiableList(list) pour exposer une vue non modifiable.

Complexités et performances (rappels)

  • Accès indexé : ArrayListO(1) amorti, LinkedListO(n).
  • Ajout en fin : ArrayListO(1) amorti, LinkedListO(1) (avec pointeur de fin).
  • Insertion/suppression au milieu par index : ArrayListO(n), LinkedListO(n) (traversée) mais O(1) si itérateur déjà positionné.

En pratique, ArrayList est souvent le meilleur choix par défaut, LinkedList gagnant seulement dans des cas niches avec des itérateurs.

Méthodes clés à connaître

Ajout, lecture, mise à jour

List<String> fruits = new ArrayList<>();
fruits.add("pomme");           // [pomme]
fruits.add("banane");          // [pomme, banane]
fruits.add(1, "poire");        // [pomme, poire, banane]

String second = fruits.get(1);  // "poire"
fruits.set(2, "prune");        // [pomme, poire, prune]

Suppression par index ou par valeur

Attention à la surcharge remove(int) vs remove(Object) :

fruits.remove(1);               // supprime l’élément à l’index 1
fruits.remove("pomme");        // supprime la première occurrence de "pomme"

Recherche et sous‑liste

int i = fruits.indexOf("prune");     // -1 si absent
List<String> debut = fruits.subList(0, 2); // vue [0,2[

subList retourne une vue adossée à la liste d’origine : modifier l’une modifie l’autre. Pour obtenir une copie indépendante :

List<String> copie = new ArrayList<>(fruits.subList(0, 2));

Tri, remplacement, filtrage

fruits.sort(Comparator.naturalOrder());     // tri croissant
fruits.replaceAll(String::toUpperCase);     // applique la fonction à chaque élément
fruits.removeIf(s -> s.length() <= 4);      // filtre en place

Itérateurs et parcours

  • Préférez le for-each pour la lecture :
for (String f : fruits) {
    System.out.println(f);
}
  • Pour supprimer en parcourant, utilisez un itérateur (ou removeIf) pour éviter ConcurrentModificationException :
Iterator<String> it = fruits.iterator();
while (it.hasNext()) {
    if (it.next().startsWith("P")) {
        it.remove(); // OK
    }
}

ListIterator : parcours bidirectionnel

ListIterator permet de parcourir dans les deux sens et d’insérer/remplacer au vol :

List<Integer> nums = new ArrayList<>(List.of(1, 2, 3));
ListIterator<Integer> li = nums.listIterator();
while (li.hasNext()) {
    Integer n = li.next();
    if (n == 2) {
        li.add(99);   // insère avant l’élément suivant
    }
}
// nums => [1, 2, 99, 3]

Immutabilité

  • Construire directement une liste immuable :
List<String> roles = List.of("ADMIN", "USER");
  • Exposer une vue non modifiable d’une liste interne :
class Service {
    private final List<String> logs = new ArrayList<>();
    public List<String> getLogs() {
        return Collections.unmodifiableList(logs);
    }
}

Concurrence

  • Lecture majoritaire, peu d’écritures : CopyOnWriteArrayList.
  • Besoin simple de synchronisation : Collections.synchronizedList(new ArrayList<>()).
  • Sinon, envisagez une conception sans partage (immutabilité, confinement de thread) ou des structures concurrentes adaptées.

Bonnes pratiques

  • Déclarez par l’interface : List<String> l = new ArrayList<>();
  • Choisissez l’implémentation selon le pattern d’accès (lecture par index ? insertions fréquentes ?).
  • Évitez LinkedList par défaut ; mesurez les perfs avant d’optimiser.
  • Attention à remove en boucle for‑each : utilisez un itérateur ou removeIf.
  • Copiez une subList si vous avez besoin d’une liste indépendante.

Quand préférer une List, un Set ou une Queue ?

  • Utilisez List si l’ordre compte et si les doublons sont permis.
  • Utilisez Set si vous devez garantir l’unicité des éléments.
  • Utilisez Queue/Deque pour des modèles FIFO/LIFO et des opérations en tête/queue optimisées.

Conclusion

List est probablement la collection la plus utilisée en Java. En comprenant ses implémentations clés, leurs complexités et les pièges courants, vous ferez des choix plus éclairés et écrirez un code plus robuste et performant.

Pour aller plus loin