W3docs

Les itérateurs Java

Parcourez les collections Java avec l'interface Iterator — hasNext, next, remove — et le contrat Iterable.

Chaque fois que vous écrivez for (T x : collection) en Java, vous faites appel à une paire cachée : l'interface Iterable<T> qui permet de parcourir la collection en boucle, et l'Iterator<T> qu'elle transmet à la boucle. La boucle for-each est du sucre syntaxique ; l'Iterator est le moteur. Comprendre ce qu'il fait — et ce que ses trois méthodes sont autorisées à lever comme exceptions — c'est la différence entre « mon parcours de liste fonctionne la plupart du temps » et « je sais exactement quand il va échouer ».

Ce chapitre porte sur le simple Iterator<E>. Le plus riche ListIterator<E>, qui peut parcourir en sens inverse et modifier pendant l'itération, a son propre chapitre juste après.

Les deux interfaces

Iterable<T> est le contrat pour « les choses que l'on peut itérer » :

public interface Iterable<T> {
  Iterator<T> iterator();
  default void forEach(Consumer<? super T> action) { ... }
  default Spliterator<T> spliterator() { ... }
}

Iterator<T> est le curseur :

public interface Iterator<E> {
  boolean hasNext();
  E next();
  default void remove() { throw new UnsupportedOperationException(); }
  default void forEachRemaining(Consumer<? super E> action) { ... }
}

Toute Collection<E> étend Iterable<E> — c'est pourquoi une boucle for-each fonctionne sur une List, un Set, une Queue. (Une Map n'est pas Iterable ; on itère sur son entrySet(), keySet() ou values() — voir comment itérer une HashMap.) La boucle for-each :

for (String name : names) { System.out.println(name); }

est compilée en :

for (Iterator<String> it = names.iterator(); it.hasNext(); ) {
  String name = it.next();
  System.out.println(name);
}

Une fois que vous avez vu ce désucrage, le reste des règles d'Iterator prend tout son sens.

Les trois méthodes, et ce qu'elles lèvent

hasNext() retourne true si next() réussirait. Idempotente — l'appeler deux fois de suite ne pose pas de problème. Ne lève jamais d'exception (dans les implémentations bien conçues).

next() avance le curseur et retourne l'élément. Lève NoSuchElementException s'il n'y a pas d'élément suivant. C'est la seule méthode d'itérateur qui lève une exception par conception lorsque vous l'utilisez incorrectement. Toujours protéger avec hasNext() s'il y a une chance que la collection soit vide :

while (it.hasNext()) { use(it.next()); }     // safe pattern

remove() supprime l'élément le plus récemment retourné par next(). C'est une méthode default qui lève UnsupportedOperationException à moins que l'itérateur ne l'implémente. ArrayList, HashMap.keySet().iterator() et similaires la prennent tous en charge. Les itérateurs retournés par List.of(...), Collections.unmodifiableList(...) et .iterator() de stream ne le font pas. Vous ne pouvez pas non plus appeler remove() deux fois de suite sans un next() intermédiaire — cela lève IllegalStateException.

Iterator<String> it = names.iterator();
while (it.hasNext()) {
  String name = it.next();
  if (name.isEmpty()) it.remove();           // legal, fail-safe removal
}

it.remove() est la seule façon sûre de supprimer d'une collection pendant l'itération avec un simple Iterator. Le propre remove(...) de la collection invaliderait l'itérateur et lèverait ConcurrentModificationException au prochain appel.

Itération fail-fast

La plupart des itérateurs de collections JDK sont fail-fast : ils enregistrent le compteur de modification de la collection lorsque l'itérateur est créé, le vérifient à chaque appel de hasNext/next, et lèvent ConcurrentModificationException s'il a changé par autre chose que l'itérateur lui-même.

List<String> names = new ArrayList<>(List.of("a", "b", "c"));
Iterator<String> it = names.iterator();
names.add("d");                              // direct mutation, not via iterator
it.next();                                   // throws ConcurrentModificationException

Le fail-fast est un diagnostic de meilleur effort, pas une garantie de sécurité des threads. Il détecte le bug courant (« j'ai modifié la liste à l'intérieur de la boucle et maintenant mon itérateur est perturbé ») de façon claire et précoce. Il ne protège pas contre la modification simultanée depuis un autre thread — pour cela, vous avez besoin d'une collection concurrente (CopyOnWriteArrayList, ConcurrentHashMap) dont les itérateurs sont faiblement cohérents à la place : ils parcourent un instantané et ne lèvent jamais d'exception.

forEachRemaining et forEach

Deux méthodes default rendent l'itération plus courte lorsque vous n'avez pas besoin du curseur :

list.forEach(System.out::println);                       // every element

Iterator<String> it = list.iterator();
while (it.hasNext() && !it.next().equals("STOP")) { }
it.forEachRemaining(System.out::println);                // everything past STOP

forEach est sur Iterable ; forEachRemaining est sur Iterator. Les deux sont séquentielles. Ne les utilisez pas quand vous avez aussi besoin de remove — elles cachent le curseur, et remove en a besoin.

Écrire votre propre Iterator

Vous en écrirez un lorsque vous implémentez un type personnalisé de type collection. Le contrat est petit, mais chaque partie compte :

class Countdown implements Iterable<Integer> {
  private final int from;
  Countdown(int from) { this.from = from; }

  @Override public Iterator<Integer> iterator() {
    return new Iterator<>() {
      int n = from;
      @Override public boolean hasNext() { return n > 0; }
      @Override public Integer next() {
        if (n <= 0) throw new NoSuchElementException();
        return n--;
      }
    };
  }
}

for (int x : new Countdown(3)) System.out.println(x);   // 3 2 1

Trois choses à bien faire :

  1. next() doit lever NoSuchElementException lorsqu'il est épuisé. Ne retournez pas null ni une valeur sentinelle.
  2. L'itérateur doit être une instance fraîche à chaque appel à iterator(). Appeler for (... : it) deux fois sur le même itérable doit repartir du début les deux fois.
  3. remove() est optionnel. Ne l'implémentez pas à moins que vous ne le puissiez réellement — le corps default qui lève une exception convient parfaitement.

Un exemple complet : itération, suppression, fail-fast, itérable personnalisé

Le programme ci-dessous parcourt un ArrayList de trois façons (for-each, itérateur explicite, forEachRemaining), supprime des éléments en toute sécurité avec Iterator.remove, démontre l'exception fail-fast lorsque vous contournez l'itérateur, et termine avec un petit Iterable<T> personnalisé.

java— editable, runs on the server

Ce qu'il faut retenir de l'exécution :

  • La boucle for-each a affiché chaque élément ; en coulisses, elle a demandé à l'ArrayList un itérateur et l'a parcouru avec hasNext/next.
  • Iterator.remove a supprimé les chaînes vides pendant l'itération sans ConcurrentModificationException. C'est la seule technique correcte de suppression dans une boucle avec un simple Iterator.
  • forEachRemaining est une façon soignée de vider ce que l'itérateur n'a pas encore produit — utile juste après un parcours partiel.
  • La mutation directe de la liste alors qu'un autre itérateur est actif a levé ConcurrentModificationException au prochain appel à next(). L'exception est intentionnelle : elle rend le bug évident.
  • Le Countdown personnalisé montre le contrat minimum nécessaire pour écrire un itérable fonctionnel. hasNext signale proprement ; next lève une exception quand il est épuisé ; pas de remove (il hérite du comportement par défaut).

La suite

Un simple Iterator peut avancer et supprimer. C'est suffisant pour les sets, les maps et les queues — ils n'ont pas de positions dans un autre sens. Les listes, elles, ont un curseur plus riche : ListIterator<E> peut se déplacer en avant et en arrière, signaler des indices, et add ou set des éléments pendant le parcours. C'est le prochain chapitre.

Pratique

Pratique
Dans une boucle `for-each` sur `List<String> list`, vous appelez `list.remove(name)` pour supprimer les entrées correspondantes. La première suppression fonctionne ; l'itération suivante lève une exception. Quelle est la bonne correction ?
Dans une boucle `for-each` sur `List<String> list`, vous appelez `list.remove(name)` pour supprimer les entrées correspondantes. La première suppression fonctionne ; l'itération suivante lève une exception. Quelle est la bonne correction ?
Was this page helpful?