Gdy trzeba przejść po elementach kolekcji i jednocześnie zachować kontrolę nad przebiegiem tej operacji, zwykła pętla nie zawsze wystarcza. Interfejs Iterator w Javie pozwala czytać elementy krok po kroku, bez znajomości wewnętrznej budowy listy, zbioru czy innej kolekcji. Pokażę, jak działa, kiedy używać metod hasNext(), next() i remove(), czym różni się od pętli for-each oraz jak uniknąć typowych wyjątków.
Iterator daje kontrolę nad przechodzeniem po kolekcji
- Iterator służy do sekwencyjnego odczytywania elementów kolekcji.
- Metoda hasNext() sprawdza, czy pozostał jeszcze element do pobrania.
- Metoda next() zwraca kolejny element i przesuwa iterator dalej.
- Element można bezpiecznie usuwać podczas iteracji za pomocą iterator.remove().
- ListIterator obsługuje dodatkowo cofanie, podmianę i wstawianie elementów.
Jak działa Iterator w Javie i gdzie go potrzebujesz
Iterator to interfejs z pakietu java.util, który udostępnia sposób przechodzenia po elementach kolekcji. Nie muszę wiedzieć, czy pracuję z ArrayList, HashSet czy własną implementacją kolekcji. Wywołuję metodę iterator(), a później pobieram kolejne wartości za pomocą jednolitego API.
Najprostszy przykład wygląda tak:
List technologie = List.of("Java", "SQL", "Docker");
Iterator iterator = technologie.iterator();
while (iterator.hasNext()) {
String technologia = iterator.next();
System.out.println(technologia);
}
Iterator działa jak kursor przesuwający się po kolekcji. Nie wskazuje elementu za pomocą indeksu, dlatego dobrze sprawdza się również przy zbiorach, które indeksów nie mają. Trzeba pamiętać, że jeden iterator przechodzi po kolekcji w określonym kierunku i ma własny stan. Jeśli chcę rozpocząć przechodzenie od początku, tworzę nowy obiekt iteratora.
Większość klas kolekcji implementuje interfejs Iterable, dzięki czemu można uzyskać iterator przez iterator(). To właśnie ten mechanizm stoi za pętlą for-each używaną z kolekcjami, chociaż w codziennym kodzie często nie trzeba tworzyć iteratora ręcznie.
Trzy podstawowe metody i ich prawidłowa kolejność
Praca z iteratorem opiera się na prostej sekwencji. Najpierw sprawdzam, czy istnieje kolejny element, potem go pobieram, a dopiero później ewentualnie usuwam. Ta kolejność jest ważniejsza, niż może się wydawać, bo każda z metod zmienia albo wykorzystuje wewnętrzny stan iteratora.
hasNext()
hasNext() zwraca wartość logiczną. Nie przesuwa iteratora, więc mogę wywołać ją kilka razy bez pomijania elementu. Najczęściej umieszcza się ją w warunku pętli while.
next()
next() zwraca następny element i przesuwa kursor. Jeśli wywołam ją po przejściu całej kolekcji, otrzymam NoSuchElementException. Z tego powodu ręczne wywoływanie next() bez wcześniejszego sprawdzenia hasNext() jest częstym błędem początkujących.
remove()
remove() usuwa ostatni element zwrócony przez next(). Nie wolno wywołać jej dwa razy z rzędu dla tego samego elementu ani przed pierwszym użyciem next(). W takich sytuacjach iterator może zgłosić IllegalStateException, a konkretna kolekcja może też nie obsługiwać usuwania i zwrócić UnsupportedOperationException.
Przeczytaj również: Java Iterable - Jak działa? Różnice, przykłady i błędy
forEachRemaining()
Ta metoda wykonuje podaną operację dla wszystkich elementów, które pozostały od bieżącej pozycji iteratora. Jest wygodna, gdy pierwszą część kolekcji przetwarzam ręcznie, a resztę chcę obsłużyć jednym wyrażeniem lambda.
Iterator iterator = technologie.iterator();
if (iterator.hasNext()) {
System.out.println("Pierwsza technologia: " + iterator.next());
}
iterator.forEachRemaining(System.out::println);
Po wykonaniu forEachRemaining() iterator znajduje się na końcu. Nie można użyć go ponownie do przejścia od początku bez utworzenia nowego iteratora.
Usuwanie elementów podczas iteracji bez błędów
Najbardziej praktyczne zastosowanie iteratora pojawia się wtedy, gdy chcę usunąć elementy w trakcie przechodzenia po kolekcji. Bezpośrednia modyfikacja listy w pętli może zakończyć się wyjątkiem ConcurrentModificationException, dlatego usuwanie wykonuję przez ten sam iterator, który aktualnie przechodzi po kolekcji.
List liczby = new ArrayList<>(
List.of(1, 2, 3, 4, 5, 6)
);
Iterator iterator = liczby.iterator();
while (iterator.hasNext()) {
Integer liczba = iterator.next();
if (liczba % 2 == 0) {
iterator.remove();
}
}
System.out.println(liczby);
Po zakończeniu działania w liście pozostaną liczby nieparzyste. Istotne jest to, że remove() usuwa element już zwrócony przez next(), a iterator prawidłowo aktualizuje swoją pozycję. Samo wywołanie liczby.remove(liczba) w tym miejscu omija tę kontrolę i może rozjechać stan iteracji.
Jeśli chodzi wyłącznie o filtrowanie kolekcji, często wybieram krótsze rozwiązanie:
liczby.removeIf(liczba -> liczba % 2 == 0);
W praktyce removeIf() jest czytelniejsze dla prostego warunku. Po iterator sięgam wtedy, gdy usuwanie jest częścią bardziej rozbudowanego procesu, zależy od kolejności odczytu albo pracuję z własną kolekcją implementującą szczególne zasady przechodzenia.
Iterator, for-each, ListIterator i strumienie
Te mechanizmy są ze sobą powiązane, ale nie służą dokładnie do tego samego. Najczęściej nie wybieram iteratora dlatego, że jest „bardziej profesjonalny”, tylko dlatego, że daje mi potrzebną kontrolę nad pozycją i modyfikacją kolekcji.
| Mechanizm | Najlepsze zastosowanie | Ograniczenie |
|---|---|---|
for-each |
Proste odczytywanie wszystkich elementów | Brak bezpośredniego sterowania kursorem i bezpiecznego usuwania |
Iterator |
Ręczne przechodzenie i usuwanie elementów | Przejście zasadniczo tylko do przodu |
ListIterator |
Praca z listą w obu kierunkach | Działa tylko dla implementacji typu List
|
| Stream API | Filtrowanie, mapowanie i agregowanie danych | Nie jest wygodnym narzędziem do ręcznego sterowania pozycją |
ListIterator rozszerza możliwości zwykłego iteratora. Pozwala używać metod hasPrevious() i previous(), a także set() do podmiany ostatnio odczytanego elementu oraz add() do wstawienia nowej wartości.
List osoby = new ArrayList<>(
List.of("Anna", "Piotr", "Marek")
);
ListIterator iterator = osoby.listIterator();
while (iterator.hasNext()) {
String osoba = iterator.next();
if (osoba.equals("Piotr")) {
iterator.set("Piotr Nowak");
}
}
W tym przypadku nie potrzebuję indeksu ani osobnego wyszukiwania elementu. ListIterator modyfikuje właściwą pozycję, zachowując kontrolę nad przechodzeniem po liście. Do samego odczytu zwykle wystarczy jednak for-each, bo kod jest krótszy i łatwiejszy do utrzymania.
Typowe błędy i ograniczenia iteratorów
Najczęściej problem nie wynika z samego interfejsu, lecz z założeń, które nie pasują do konkretnej kolekcji. Iterator nie gwarantuje na przykład kolejności elementów, jeśli korzystam z kolekcji, która tej kolejności nie definiuje, takiej jak zwykły HashSet.
- Nie wywołuj next() bez hasNext(), jeśli nie masz pewności, że element istnieje.
-
Nie modyfikuj kolekcji bezpośrednio podczas iteracji, gdy możesz użyć
iterator.remove(). -
Nie zakładaj, że remove() zawsze działa. Kolekcje niemodyfikowalne, na przykład utworzone przez
List.of(), nie pozwalają na usuwanie. - Nie używaj jednego iteratora ponownie po dojściu do końca. Utwórz nowy.
- Nie traktuj wyjątku fail-fast jako mechanizmu synchronizacji. Jest ostrzeżeniem o niezgodnej modyfikacji, a nie ochroną wielowątkową.
Warto też pamiętać, że iterator zwykle nie tworzy kopii kolekcji. Pracuje na jej aktualnym widoku i może reagować na modyfikacje zależnie od implementacji. W aplikacji wielowątkowej potrzebna jest odpowiednia kolekcja współbieżna albo synchronizacja, a nie samo zastąpienie pętli iteratorem.
Osobną pułapką jest próba używania iteratora do dostępu losowego. Jeśli regularnie potrzebuję elementu o indeksie 5000, iterator będzie niewygodny i potencjalnie kosztowny. Do takiego zadania lepiej pasuje lista z dostępem indeksowym, a iterator pozostaje narzędziem do sekwencyjnego przechodzenia.
Najprostsza zasada wyboru właściwego rozwiązania
Gdy tylko odczytuję elementy kolekcji, zaczynam od pętli for-each. Jest czytelna i w większości przypadków wystarcza. Po Iterator sięgam wtedy, gdy muszę ręcznie kontrolować postęp, usuwać elementy podczas przechodzenia albo pracuję z typem kolekcji, którego nie obsługuje wygodnie zwykła pętla.
Jeśli potrzebuję poruszać się w obu kierunkach lub zmieniać elementy listy, wybieram ListIterator. Do prostego filtrowania preferuję removeIf(), a do przekształcania danych często lepszy będzie Stream API. Ta decyzja zwykle prowadzi do krótszego kodu i wyraźniej pokazuje intencję programu.
Najważniejsze jest zapamiętanie jednej sekwencji: sprawdź hasNext(), pobierz next(), a potem ewentualnie użyj remove(). Kiedy ten schemat staje się naturalny, iterator przestaje być abstrakcyjnym interfejsem z dokumentacji, a zaczyna być praktycznym narzędziem do bezpiecznej pracy z kolekcjami.