Iterator w Javie - jak bezpiecznie przechodzić po kolekcji

Java Kolekcje i listy: kod z użyciem java iterator do iteracji po liście. Diagram struktur danych.

Napisano przez

Tymoteusz Sobczak

Opublikowano

2 paź 2026

Spis treści

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.

FAQ - Najczęstsze pytania

Po Iterator sięgnij, gdy musisz ręcznie kontrolować postęp przechodzenia po kolekcji albo usuwać elementy podczas iteracji. Do prostego odczytu wszystkich wartości for-each jest zwykle krótsza i czytelniejsza.

Po pobraniu elementu przez next() użyj iterator.remove(). Bezpośrednie wywołanie metody usuwającej na kolekcji może zakończyć się wyjątkiem ConcurrentModificationException. Przy prostym filtrowaniu alternatywą jest removeIf().

next() bez dostępnego elementu może zgłosić NoSuchElementException. remove() wywołane przed next() lub dwa razy z rzędu może zakończyć się IllegalStateException, a kolekcja niemodyfikowalna może zwrócić UnsupportedOperationException.

ListIterator działa z listami i pozwala poruszać się w obu kierunkach za pomocą hasPrevious() i previous(). Umożliwia także podmianę elementu przez set() oraz wstawianie wartości przez add().

Do prostego usuwania elementów według warunku użyj removeIf(). Stream API lepiej sprawdzi się przy filtrowaniu, mapowaniu i agregowaniu danych, natomiast Iterator jest właściwy, gdy potrzebujesz ręcznej kontroli nad pozycją i przebiegiem iteracji.

Oceń artykuł

Ocena: 0.00 Liczba głosów: 0

Tagi:

kolekcje iterator listiterator stream api for-each

Udostępnij artykuł

Tymoteusz Sobczak

Tymoteusz Sobczak

Nazywam się Tymoteusz Sobczak i od 7 lat zajmuję się programowaniem webowym. Moje zainteresowanie tą dziedziną zaczęło się od prostych projektów, które realizowałem w wolnym czasie. Z czasem odkryłem, jak fascynujące jest tworzenie aplikacji, które mogą ułatwiać życie innym. Chętnie dzielę się swoją wiedzą, pomagając czytelnikom zrozumieć złożone zagadnienia związane z programowaniem, od podstawowych technik po bardziej zaawansowane rozwiązania. Pisząc na temat programowania, staram się dostarczać informacje, które są nie tylko użyteczne, ale także zrozumiałe. Zawsze dokładam starań, aby moje źródła były wiarygodne, a treści aktualne. Lubię upraszczać trudne tematy i organizować wiedzę w sposób, który ułatwia naukę. Wierzę, że każdy, kto ma chęci, może stać się dobrym programistą, a ja jestem tutaj, aby wskazać drogę.

Napisz komentarz