Menu

Pętla for-each w Javie: składnia, przykłady i pułapki

Pętla for-each (rozszerzona pętla for) w Javie: czytelne przechodzenie po tablicach i kolekcjach, kiedy jej używać i pułapka modyfikacji, na którą każdy trafia.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

Gdy indeks tylko przeszkadza

Pętla for z licznikiem daje ci licznik, warunek i krok aktualizacji. Bardzo często jednak pozycja elementu w ogóle cię nie interesuje: chcesz po prostu coś zrobić z każdym elementem, po kolei, od początku do końca. Zarządzanie indeksem to wtedy zbędna robota i właśnie tam wkradają się błędy o jeden.

Pętla for-each (w Javie nazywana rozszerzonym for) całkowicie rezygnuje z licznika. Nazywasz zmienną, wskazujesz kolekcję, a pętla podaje ci kolejne elementy.

Podstawowa składnia

Schemat to for (Type element : collection). Dwukropek czytaj jako słowo „w”:

Nie ma i, nie ma scores.length, nie ma scores[i]. W każdym przebiegu score jest kolejnym elementem. Pętla wykonuje się raz dla każdego elementu i zatrzymuje się automatycznie, gdy elementy się skończą: nie da się wyjść poza koniec ani zacząć o jeden element za wcześnie.

Pętla po liście

Ta sama pętla działa na wszystkim, po czym da się iterować, czyli między innymi na List, Set i innych typach kolekcji. Typ elementu stoi przed nazwą zmiennej:

Zauważ, że nie trzeba wiedzieć, czy langs opiera się na tablicy, liście wiązanej czy czymś innym: for-each działa tak samo na wszystkich. To jej prawdziwa siła: jedna czytelna składnia dla każdej kolekcji.

var oszczędza pisania nazwy typu

Jeśli typ elementu jest długi albo oczywisty, var pozwala kompilatorowi go wywnioskować, więc nie musisz się powtarzać:

Bez var zmienna pętli miałaby rozwlekły typ Map.Entry<String, Integer>. var utrzymuje czytelność, a typ nadal jest w pełni sprawdzany podczas kompilacji: to nie jest luźny, dynamiczny typ.

Pułapka modyfikacji

Oto zasada, na którą trafia każdy: nie możesz dodawać elementów do kolekcji ani ich z niej usuwać, gdy przechodzi po niej pętla for-each. Takie działanie rzuca ConcurrentModificationException:

List<String> items = new ArrayList<>(List.of("a", "b", "c"));

for (String item : items) {
    if (item.equals("b")) {
        items.remove(item);   // rzuca ConcurrentModificationException
    }
}

Pętla zauważa, że lista zmieniła się pod nią, i przerywa działanie, zamiast po cichu pomijać lub powtarzać elementy. Aby bezpiecznie usuwać, zejdź poziom niżej do jawnego Iterator, który ma metodę remove() znaną pętli:

Popularny skrót to items.removeIf(item -> item.equals("b")), który używa wyrażenia lambda, aby zrobić to samo w jednym wierszu.

Tylko odczyt, bez przypisywania

Kolejne subtelne ograniczenie: przypisanie wartości do zmiennej pętli zmienia tylko lokalną kopię, a nie kolekcję. Zaskakuje to osoby przychodzące z języków, w których zmienna pętli jest żywą referencją:

Jeśli musisz zapisywać z powrotem do tablicy, potrzebujesz indeksu, a to oznacza klasyczną pętlę for z licznikiem: for (int i = 0; i < nums.length; i++) nums[i] = nums[i] * 10;. W przypadku elementów będących obiektami możesz zmieniać obiekt, na który wskazuje zmienna (na przykład wywołując setter), ale nie możesz podmienić go w kolekcji.

break i continue nadal działają

For-each to prawdziwa pętla, więc break i continue działają dokładnie tak jak gdzie indziej: break opuszcza pętlę, a continue przechodzi do następnego elementu:

Program wypisuje keep, a potem znowu keep: pomija "skip" i zatrzymuje się na "stop", zanim dotrze do "never". Nie musisz więc odwiedzać każdego elementu; rezygnujesz tylko z indeksu w zamian za czytelniejszy kod.

Dalej: tablice

Tablice pojawiły się już w kilku pętlach, ale bez wyjaśnienia, czym dokładnie są: pojemnikami o stałym rozmiarze, z indeksami i polem .length. Następna strona wraca do początku i porządnie omawia tablice: ich deklarowanie, wartości domyślne na start i to, czym ich stały rozmiar różni się od rozszerzalnej ArrayList.

Najczęściej zadawane pytania

Czym jest pętla for-each w Javie?

Pętla for-each (nazywana też rozszerzonym for) przechodzi po każdym elemencie tablicy lub kolekcji bez licznika: for (Type item : collection) { ... }. Czytaj ją jako „dla każdego elementu w kolekcji”. Jest czytelniejsza niż pętla for z licznikiem, gdy potrzebujesz tylko kolejnych elementów, a nigdy indeksu.

Czym różni się pętla for od pętli for-each w Javie?

Klasyczna pętla for używa jawnego licznika (for (int i = 0; i < arr.length; i++)), więc kontrolujesz indeks i kierunek. Pętla for-each, for (Type x : arr), nie ma indeksu: odwiedza każdy element po kolei. Używaj for-each do przejść tylko do odczytu, od początku do końca, a pętli z licznikiem, gdy potrzebujesz indeksu, chcesz pomijać elementy albo zmieniać strukturę kolekcji.

Dlaczego moja pętla for-each rzuca ConcurrentModificationException?

Bo wywołujesz add() albo remove() na kolekcji, po której iteruje pętla for-each. Pętla wykrywa zmianę struktury i rzuca wyjątek, aby chronić cię przed niezdefiniowanym zachowaniem. Aby bezpiecznie usuwać elementy, użyj jawnego Iterator i jego metody remove() albo zbierz elementy do usunięcia i usuń je po pętli.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ