W3docs

Java Iteratoren

Java-Collections mit dem Iterator-Interface durchlaufen — hasNext, next, remove — und der Iterable-Vertrag.

Jedes Mal, wenn Sie in Java for (T x : collection) schreiben, greifen Sie auf ein verstecktes Paar zurück: das Iterable<T>-Interface, das der Collection erlaubt, durchlaufen zu werden, und den Iterator<T>, den es der Schleife übergibt. Die for-each-Schleife ist syntaktischer Zucker; der Iterator ist der Motor. Zu verstehen, was er tut — und welche Ausnahmen seine drei Methoden werfen dürfen — ist der Unterschied zwischen „mein Listen-Traversal funktioniert meistens" und „ich weiß genau, wann er fehlschlägt."

Dieses Kapitel behandelt den einfachen Iterator<E>. Der reichhaltigere ListIterator<E>, der rückwärts laufen und während der Iteration modifizieren kann, bekommt ein eigenes Kapitel als nächstes.

Die zwei Interfaces

Iterable<T> ist der Vertrag für „Dinge, die iteriert werden können":

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

Iterator<T> ist der Cursor:

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

Jede Collection<E> erweitert Iterable<E> — deshalb funktioniert eine for-each-Schleife auf einer List, einem Set und einer Queue. (Eine Map ist nicht Iterable; Sie iterieren über ihr entrySet(), keySet() oder values() — siehe wie man eine HashMap iteriert.) Die for-each-Schleife:

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

wird kompiliert zu:

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

Sobald Sie diese Entzuckerung gesehen haben, ergeben die restlichen Regeln des Iterator Sinn.

Die drei Methoden und was sie werfen

hasNext() gibt true zurück, wenn next() erfolgreich wäre. Idempotent — es zweimal hintereinander aufzurufen ist in Ordnung. Wirft nie (bei gut implementierten Implementierungen).

next() rückt den Cursor vor und gibt das Element zurück. Wirft NoSuchElementException, wenn kein nächstes Element vorhanden ist. Das ist die einzige Iterator-Methode, die bei Missbrauch absichtlich eine Ausnahme wirft. Sichern Sie immer mit hasNext() ab, wenn die Collection leer sein könnte:

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

remove() entfernt das zuletzt von next() zurückgegebene Element. Es ist eine default-Methode, die UnsupportedOperationException wirft, sofern der Iterator sie nicht implementiert. ArrayList, HashMap.keySet().iterator() und ähnliche unterstützen es. Iteratoren, die von List.of(...), Collections.unmodifiableList(...) und Stream .iterator() zurückgegeben werden, tun dies nicht. Sie können remove() auch nicht zweimal hintereinander aufrufen, ohne ein dazwischenliegendes next() — das wirft IllegalStateException.

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

it.remove() ist der einzig sichere Weg, Elemente aus einer Collection zu entfernen, während man mit einem einfachen Iterator iteriert. Das eigene remove(...) der Collection würde den Iterator ungültig machen und ConcurrentModificationException beim nächsten Aufruf werfen.

Fail-fast-Iteration

Die meisten JDK-Collection-Iteratoren sind fail-fast: Sie zeichnen die Modifikationsanzahl der Collection auf, wenn der Iterator erstellt wird, prüfen sie bei jedem hasNext/next-Aufruf und werfen ConcurrentModificationException, wenn sie von jemand anderem als dem Iterator selbst geändert wurde.

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

Fail-fast ist eine Best-Effort-Diagnose, keine Garantie für Thread-Sicherheit. Es erkennt den häufigen Fehler („oh, ich habe die Liste innerhalb der Schleife geändert und jetzt ist mein Iterator verwirrt") klar und frühzeitig. Es schützt nicht vor gleichzeitiger Änderung durch einen anderen Thread — dafür benötigt man eine concurrent Collection (CopyOnWriteArrayList, ConcurrentHashMap), deren Iteratoren stattdessen schwach konsistent sind: Sie durchlaufen einen Snapshot und werfen nie.

forEachRemaining und forEach

Zwei Default-Methoden machen die Iteration kürzer, wenn man den Cursor nicht braucht:

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 ist auf Iterable; forEachRemaining auf Iterator. Beide sind sequenziell. Verwenden Sie sie nicht, wenn Sie auch remove benötigen — sie verbergen den Cursor, und remove erfordert ihn.

Einen eigenen Iterator schreiben

Sie schreiben einen, wenn Sie einen benutzerdefinierten collection-ähnlichen Typ implementieren. Der Vertrag ist klein, aber jeder Teil davon ist wichtig:

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

Drei Dinge, die man richtig machen muss:

  1. next() muss NoSuchElementException werfen, wenn erschöpft. Geben Sie kein null oder einen Sentinel-Wert zurück.
  2. Der Iterator sollte bei jedem Aufruf von iterator() eine neue Instanz sein. for (... : it) zweimal auf demselben Iterable aufzurufen, sollte beide Male von vorne beginnen.
  3. remove() ist optional. Implementieren Sie es nicht, wenn Sie es nicht können — der default-Rumpf, der wirft, ist in Ordnung.

Ein vollständiges Beispiel: Iteration, Entfernung, Fail-fast, eigenes Iterable

Das Programm unten durchläuft eine ArrayList auf drei Arten (for-each, expliziter Iterator, forEachRemaining), entfernt Elemente sicher mit Iterator.remove, demonstriert die Fail-fast-Ausnahme, wenn man den Iterator umgeht, und schließt mit einem kleinen benutzerdefinierten Iterable<T>.

java— editable, runs on the server

Was man aus dem Lauf mitnehmen kann:

  • Die for-each-Schleife hat jedes Element ausgegeben; im Hintergrund hat sie die ArrayList nach einem Iterator gefragt und ihn mit hasNext/next durchlaufen.
  • Iterator.remove hat die leeren Strings während der Iteration gelöscht, ohne eine ConcurrentModificationException. Das ist die einzige korrekte In-Loop-Löschmethode mit einem einfachen Iterator.
  • forEachRemaining ist eine ordentliche Möglichkeit, alles zu verarbeiten, was der Iterator noch nicht geliefert hat — nützlich direkt nach einem teilweisen Durchlauf.
  • Das direkte Mutieren der Liste, während ein anderer Iterator aktiv war, hat ConcurrentModificationException beim nächsten next()-Aufruf geworfen. Die Ausnahme ist beabsichtigt: Sie macht den Fehler laut.
  • Das benutzerdefinierte Countdown zeigt den minimalen Vertrag, den man braucht, um ein funktionierendes Iterable zu schreiben. hasNext berichtet sauber; next wirft, wenn erschöpft; kein remove (es erbt den Standard).

Was kommt als nächstes

Ein einfacher Iterator kann vorwärts laufen und entfernen. Das reicht für Sets, Maps und Queues — sie haben in keinem anderen Sinne Positionen. Listen haben das, und sie erhalten einen reichhaltigeren Cursor: ListIterator<E> kann sich vorwärts und rückwärts bewegen, Indizes berichten und Elemente während des Durchlaufs adden oder setzen. Das ist das nächste Kapitel.

Übungen

Übung
Innerhalb einer `for-each`-Schleife über `List<String> list` rufen Sie `list.remove(name)` auf, um passende Einträge zu löschen. Das erste Entfernen funktioniert; die nächste Iteration wirft. Was ist die richtige Lösung?
Innerhalb einer `for-each`-Schleife über `List<String> list` rufen Sie `list.remove(name)` auf, um passende Einträge zu löschen. Das erste Entfernen funktioniert; die nächste Iteration wirft. Was ist die richtige Lösung?
Was this page helpful?