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 patternremove() 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 ConcurrentModificationExceptionLe 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 STOPforEach 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 1Trois choses à bien faire :
next()doit leverNoSuchElementExceptionlorsqu'il est épuisé. Ne retournez pasnullni une valeur sentinelle.- L'itérateur doit être une instance fraîche à chaque appel à
iterator(). Appelerfor (... : it)deux fois sur le même itérable doit repartir du début les deux fois. remove()est optionnel. Ne l'implémentez pas à moins que vous ne le puissiez réellement — le corpsdefaultqui 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é.
Ce qu'il faut retenir de l'exécution :
- La boucle for-each a affiché chaque élément ; en coulisses, elle a demandé à l'
ArrayListun itérateur et l'a parcouru avechasNext/next. Iterator.removea supprimé les chaînes vides pendant l'itération sansConcurrentModificationException. C'est la seule technique correcte de suppression dans une boucle avec un simpleIterator.forEachRemainingest 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é
ConcurrentModificationExceptionau prochain appel ànext(). L'exception est intentionnelle : elle rend le bug évident. - Le
Countdownpersonnalisé montre le contrat minimum nécessaire pour écrire un itérable fonctionnel.hasNextsignale proprement ;nextlève une exception quand il est épuisé ; pas deremove(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.