Jedno zadanie, kilka narzędzi
Masz już zebrane dane w ArrayList, HashSet albo HashMap i chcesz odwiedzić każdy element. Java daje na to kilka sposobów, a wybór zależy od tego, czy potrzebujesz indeksu, czy musisz usuwać elementy w trakcie pętli i czy wolisz składnię metody, czy pętli.
Dobra wiadomość: każda Collection (lista, zbiór, kolejka) obsługuje tę samą ulepszoną pętlę for, więc gdy ją poznasz, po wszystkich przejdziesz w ten sam sposób.
Dwukropek czytaj jako „w”: „dla każdego lang w langs”. Nie dotykasz żadnego indeksu, więc nie ma tu czego zepsuć.
Pętla for-each
Ulepszony for to domyślny wybór do prostego „zrób coś z każdym elementem”. Czyta się go przejrzyście i działa tak samo dla wszystkich typów kolekcji. Tutaj HashSet, który w ogóle nie ma indeksów:
O HashSet warto pamiętać jedno: nie ma on określonej kolejności, więc elementy mogą wypisać się w dowolnym porządku. For-each i tak odwiedzi każdy z nich dokładnie raz.
Gdy potrzebujesz indeksu
For-each daje element, ale nie jego pozycję. Jeśli naprawdę potrzebujesz indeksu, na przykład żeby numerować wiersze albo zajrzeć do sąsiednich elementów, użyj pętli z licznikiem, size() i get(i). Działa to na List, która jest pozycyjna. Zbiory i mapy nie mają indeksu, więc ten styl ich nie dotyczy.
Nie sięgaj po to z samego przyzwyczajenia. Jeśli i służy ci tylko do get(i), wersja z for-each jest krótsza i trudniej w niej o błąd.
Iterowanie po Map
Map nie jest Collection, więc nie da się po niej przejść for-each bezpośrednio. Zamiast tego przechodzisz po jednym z jej trzech widoków. Najczęściej używa się entrySet(), który podaje każdą parę klucz-wartość razem:
Jeśli potrzebujesz tylko kluczy, przejdź po ages.keySet(). Jeśli tylko wartości, po ages.values(). Gdy potrzebujesz obu, wybierz entrySet(): pętla po kluczach z wywołaniem ages.get(key) w środku bez powodu wykonuje dodatkowe wyszukiwanie w każdej iteracji.
Iterator
For-each to w rzeczywistości lukier składniowy nad Iterator, czyli obiektem, który przechodzi po kolekcji element po elemencie przez hasNext() i next(). Rzadko piszesz tę pętlę ręcznie, z jednym ważnym wyjątkiem: to bezpieczny sposób na usuwanie elementów podczas iteracji.
it.remove() usuwa element, który ostatnio zwróciło next(), a iterator pozostaje poprawny. To jedyny dozwolony sposób modyfikowania kolekcji podczas ręcznej pętli.
Pułapka: ConcurrentModificationException
Jeśli wewnątrz pętli for-each wywołasz add lub remove na samej kolekcji, dostaniesz ConcurrentModificationException: iterator zauważa, że kolekcja zmieniła się pod nim, i odmawia dalszej pracy. To jeden z najczęstszych błędów początkujących i jak każdy wyjątek czasu wykonania przerywa metodę, chyba że unikniesz jego wywołania.
List<Integer> nums = new ArrayList<>(List.of(1, 2, 3, 4));
for (int n : nums) {
if (n % 2 == 0) {
nums.remove(Integer.valueOf(n)); // throws ConcurrentModificationException
}
}
Rozwiązaniem jest prawie zawsze removeIf, które wyraża intencję w jednej linii i samo zajmuje się iteracją:
removeIf działa na każdej Collection, więc to samo wywołanie wyczyści też HashSet.
Metoda forEach
Każda kolekcja ma też metodę forEach, która przyjmuje lambdę i uruchamia ją dla każdego elementu. To bardziej funkcyjna alternatywa dla pętli, w stylu wyrażenia, wygodna przy krótkich jednolinijkowcach:
Zwróć uwagę, że Map.forEach przyjmuje bezpośrednio lambdę z dwoma argumentami (key, value), bez potrzeby entrySet(). Używaj forEach do szybkich efektów ubocznych. Wróć do zwykłej pętli for, gdy ciało się rozrasta albo musisz wcześniej wyjść przez break, czego lambda nie potrafi.
Dalej: metody
Dane są już spakowane w kolekcje i umiesz przejść po nich na każdy sposób, jaki oferuje Java. Następny krok to pakowanie zachowania: pisanie własnych metod, dzięki którym nazwiesz blok logiki, przekażesz mu dane wejściowe i użyjesz go ponownie. O tym jest następna strona.
Najczęściej zadawane pytania
Jak przejść pętlą po liście w Javie?
Najczytelniej zrobić to ulepszoną pętlą for (for-each): for (String s : list) { ... }. Działa z każdą Collection: ArrayList, HashSet i tak dalej. Pętli z indeksem i get(i) używaj tylko wtedy, gdy naprawdę potrzebujesz pozycji, a Iterator wtedy, gdy musisz usuwać elementy w trakcie pętli.
Jak iterować po HashMap w Javie?
Map nie jest Collection, więc przechodzisz po jednym z jej widoków. Zwykle wybiera się for (Map.Entry<K, V> e : map.entrySet()), co w jednym przebiegu daje zarówno klucz (e.getKey()), jak i wartość (e.getValue()). Możesz też przejść po map.keySet(), żeby dostać klucze, albo po map.values(), żeby dostać wartości.
Dlaczego podczas pętli dostaję ConcurrentModificationException?
Wywołano add lub remove na kolekcji w czasie, gdy pętla for-each po niej iterowała. Pętla for-each używa pod spodem Iterator, a ten wykrywa, że struktura kolekcji się zmieniła. Rozwiązanie: usuwaj elementy metodą remove() samego Iterator albo zamiast pętli wywołaj removeIf(...).