Java ListIterator
Parcourez les listes Java dans les deux sens et modifiez-les durant l'itération avec l'interface ListIterator.
ListIterator<E> étend Iterator<E> avec tout ce qu'une liste peut prendre en charge mais qu'un itérable générique ne peut pas : parcourir en arrière, obtenir l'index courant, et ajouter ou remplacer des éléments durant l'itération. Il est disponible sur chaque List<E> via list.listIterator() et list.listIterator(int startAt).
Si vous itérez un Set ou une Queue, ce chapitre ne s'applique pas — ces collections n'ont pas de positions. Pour List, ListIterator est le curseur qui fait tout ce que fait le simple Iterator, plus les quatre opérations spécifiques aux listes.
Ce que ListIterator ajoute
public interface ListIterator<E> extends Iterator<E> {
// inherited:
boolean hasNext();
E next();
void remove();
// new:
boolean hasPrevious();
E previous();
int nextIndex();
int previousIndex();
void set(E e);
void add(E e);
}Trois nouvelles capacités :
- Parcours bidirectionnel.
hasPrevious()/previous()déplacent le curseur vers l'arrière.previous()lèveNoSuchElementExceptionavant le début. - Rapport de position.
nextIndex()retourne l'index quenext()retournerait ;previousIndex()retourne l'index queprevious()retournerait. Ils diffèrent de 1. - Éditions en place.
set(e)remplace l'élément le plus récemment retourné parnextouprevious.add(e)insère un nouvel élément entre les positions précédente et suivante du curseur.
Le modèle de curseur
La clé pour comprendre ListIterator est d'imaginer le curseur comme se trouvant entre les éléments, pas sur eux :
[ "a" "b" "c" ]
^ ^ ^ ^
0 1 2 3 <- nextIndex() valuesnext() retourne l'élément à droite du curseur et avance. previous() retourne l'élément à gauche et recule. Juste après que next() retourne "b" :
[ "a" "b" "c" ]
^
previousIndex()=1, nextIndex()=2Un set("B") suivant remplace "b". Un add("x") suivant insère "x" entre "b" et "c". Un remove() suivant supprime "b". Un seul appel parmi set, add ou remove peut être effectué une fois par next/previous — appeler deux d'affilée, ou en appeler un sans next/previous intermédiaire, lève IllegalStateException.
Parcours bidirectionnel
List<String> letters = new ArrayList<>(List.of("a", "b", "c"));
ListIterator<String> it = letters.listIterator();
while (it.hasNext()) System.out.print(it.next() + " "); // a b c
while (it.hasPrevious()) System.out.print(it.previous() + " "); // c b aLes deux boucles utilisent le même itérateur. Une fois la boucle avant épuisée, le curseur est passé "c" ; la boucle arrière commence là et revient au début. Vous pouvez aussi changer de direction en cours de parcours — appelez next() puis previous() et vous obtenez le même élément en retour les deux fois, car le curseur l'a dépassé puis est revenu dessus.
Édition en place durant l'itération
C'est la principale raison d'utiliser ListIterator plutôt qu'un simple Iterator :
List<String> words = new ArrayList<>(List.of("alpha", "beta", "gamma"));
ListIterator<String> it = words.listIterator();
while (it.hasNext()) {
String w = it.next();
if (w.startsWith("a")) it.set(w.toUpperCase()); // replace in place
if (w.equals("beta")) it.add("BETA-extra"); // insert after beta
}
// words is now [ALPHA, beta, BETA-extra, gamma]set est le seul moyen sûr de remplacer un élément durant l'itération. add est le seul moyen sûr d'insérer un élément durant l'itération. Les deux mettent à jour le compteur de modifications attendues de l'itérateur, donc aucun des deux ne déclenche de ConcurrentModificationException.
add mérite un second regard : il insère au niveau du curseur — entre le résultat du next le plus récent et le prochain résultat de next. Après l'insertion, le curseur est passé le nouvel élément, donc le tout prochain it.next() retourne l'élément suivant original, pas celui que vous venez d'ajouter. C'est presque toujours le comportement souhaité lorsque vous "développez" un élément en place.
Un piège courant : previous() retourne le même élément que vous venez de retourner avec next()
ListIterator<String> it = letters.listIterator();
it.next(); // "a", cursor between a and b
it.previous(); // "a" again, cursor between (start) and aCela trompe les gens. La position du curseur change après next, mais previous revient en arrière sur le même élément. Si vous voulez l'élément avant le courant, vous devez appeler previous deux fois — une fois pour revenir en arrière sur ce que vous venez de retourner, une fois pour lire réellement le précédent.
Démarrer à un index spécifique
ListIterator<String> it = list.listIterator(3); // start with cursor before index 3La forme à deux arguments positionne le curseur avant l'index donné. it.nextIndex() retourne 3, it.previousIndex() retourne 2, et le tout premier next() retourne list.get(3). Utile lorsque vous avez déjà localisé un point de départ avec indexOf ou binarySearch et souhaitez parcourir à partir de là dans l'une ou l'autre direction.
LinkedList vs ArrayList : même interface, coût différent
Les deux exposent ListIterator. Le profil de coût diffère :
ArrayList—next/previoussont O(1) ;add/removedurant l'itération sont O(n) car ils décalent la queue du tableau. L'opérationsetreste O(1).LinkedList—next/previoussont O(1) (l'itérateur met en cache le nœud) ;add/removevia l'itérateur sont O(1) car aucun décalage ne se produit. Les mêmes opérations par index sur uneLinkedListsont O(n) — les recherches par index parcourent la chaîne.
Si vous itérez une LinkedList et appelez list.add(index, ...) à l'intérieur de la boucle, vous parcourez la chaîne deux fois par insertion. Utilisez le ListIterator et vous payez O(1) par opération, ce qui est la raison d'être de LinkedList.
Un exemple concret : parcours bidirectionnel, éditions en place, rapport d'index, coût des opérations
Le programme ci-dessous parcourt une liste en avant et en arrière sur le même itérateur, remplace et insère des éléments en place, rapporte les index au fur et à mesure, et mesure la différence entre la modification basée sur l'itérateur et la modification basée sur l'index sur une LinkedList.
Ce qu'il faut retenir de l'exécution :
- Les parcours en avant et en arrière se font sur le même
ListIterator. Une fois que la boucle vers l'avant épuisehasNext, le curseur est passé le dernier élément ethasPreviousdevient vrai. seta remplacé"alpha"par"ALPHA"etadd("BETA-extra")a inséré un nouvel élément juste après"beta"— et l'itérateur a survécu aux deux modifications sansConcurrentModificationException.next()puisprevious()ont retourné le même élément. Le curseur l'a dépassé puis est revenu dessus ; ce qui ressemblait à deux lectures d'éléments "différents" est en réalité un seul élément parcouru deux fois.- Sur une
LinkedList, la version basée sur l'itérateur de "supprimer un élément sur deux" était considérablement plus rapide que la version basée sur l'index. La recherche par index sur une liste chaînée est O(n) ; l'itérateur met en cache son nœud et la suppression est O(1).
La suite
Iterator et ListIterator gèrent le côté traversal du travail avec les collections. L'autre moitié de "faire des choses avec les éléments" est de les ordonner : dire à Java quand un élément est inférieur, égal ou supérieur à un autre. C'est ce que couvrent Comparable et Comparator — l'ordre naturel intégré au type lui-même, et les ordres externes que vous fournissez par opération. Ils constituent le fondement sur lequel repose tout le reste dans cette partie du livre, y compris les utilitaires de tri et de recherche.