Wstęp
Pętle są jedną z podstawowych struktur języków programowania. Przykładowo na stronie Oracle znajduje się sekcja „
Lekcja: Podstawy języka ”, w której pętle mają osobną lekcję „
Instrukcja for ”. Odświeżmy podstawy: Pętla składa się z trzech wyrażeń (instrukcji):
inicjalizacji (inicjalizacji),
warunku (zakończenia) i
inkrementacji (inkrementacji):
Co ciekawe, wszystkie są opcjonalne, co oznacza, że jeśli chcemy, możemy napisać:
for (;;){
}
To prawda, że w tym przypadku otrzymamy nieskończoną pętlę, ponieważ Nie podajemy warunku wyjścia z pętli (zakończenia). Wyrażenie inicjujące jest wykonywane tylko raz, przed wykonaniem całej pętli. Zawsze warto pamiętać, że cykl ma swój zakres. Oznacza to, że
inicjalizacja ,
zakończenie ,
inkrementacja i treść pętli widzą te same zmienne. Zakres można zawsze łatwo określić za pomocą nawiasów klamrowych. Wszystko, co jest w nawiasach, nie jest widoczne poza nawiasami, ale wszystko, co jest poza nawiasami, jest widoczne w nawiasach.
Inicjalizacja to tylko wyrażenie. Na przykład zamiast inicjować zmienną, można ogólnie wywołać metodę, która niczego nie zwróci. Lub po prostu pomiń, pozostawiając spację przed pierwszym średnikiem. Poniższe wyrażenie określa
warunek zakończenia . Jeśli jest to
prawdą , pętla jest wykonywana. A jeśli
false , nowa iteracja nie rozpocznie się. Jeśli spojrzysz na poniższy obrazek, podczas kompilacji pojawi się błąd, a IDE będzie narzekać: nasze wyrażenie w pętli jest nieosiągalne. Ponieważ nie będziemy mieli ani jednej iteracji w pętli, natychmiast zakończymy działanie, ponieważ FAŁSZ:
Warto zwrócić uwagę na wyrażenie w
instrukcji zakończenia : bezpośrednio określa to, czy Twoja aplikacja będzie miała nieskończone pętle.
Inkrementacja jest najprostszym wyrażeniem. Jest wykonywany po każdej udanej iteracji pętli. I to wyrażenie można również pominąć. Na przykład:
int outerVar = 0;
for (;outerVar < 10;) {
outerVar += 2;
System.out.println("Value = " + outerVar);
}
Jak widać z przykładu, w każdej iteracji pętli będziemy zwiększać o 2 przyrosty, ale tylko pod warunkiem, że wartość
outerVar
będzie mniejsza niż 10. Ponadto, ponieważ wyrażenie w
instrukcji inkrementacji jest w rzeczywistości tylko wyrażeniem, może zawierać wszystko. Dlatego nikt nie zabrania stosowania ubytku zamiast przyrostu, tj. zmniejszyć wartość. Należy zawsze monitorować zapisywanie przyrostu.
+=
najpierw wykonuje zwiększenie, a potem przypisanie, ale jeśli w powyższym przykładzie napiszemy odwrotnie, otrzymamy nieskończoną pętlę, ponieważ zmienna
outerVar
nigdy nie otrzyma zmienionej wartości: w tym przypadku
=+
zostanie ona wyliczona po przypisaniu. Nawiasem mówiąc, to samo dotyczy przyrostów wyświetleń
++
. Na przykład mieliśmy pętlę:
String[] names = {"John","Sara","Jack"};
for (int i = 0; i < names.length; ++i) {
System.out.println(names[i]);
}
Cykl zadziałał i nie było żadnych problemów. Ale wtedy przyszedł człowiek zajmujący się refaktoryzacją. Nie zrozumiał przyrostu i po prostu zrobił to:
String[] names = {"John","Sara","Jack"};
for (int i = 0; i < names.length;) {
System.out.println(names[++i]);
}
Jeśli przed wartością pojawi się znak zwiększenia, oznacza to, że najpierw wzrośnie, a następnie powróci do miejsca, w którym jest wskazany. W tym przykładzie od razu zaczniemy wyodrębniać z tablicy element o indeksie 1, pomijając pierwszy. A następnie przy indeksie 3 nastąpi awaria z błędem „
java.lang.ArrayIndexOutOfBoundsException ”. Jak można się domyślić, działało to wcześniej po prostu dlatego, że inkrementacja jest wywoływana po zakończeniu iteracji. Przy przenoszeniu tego wyrażenia do iteracji wszystko się zepsuło. Jak się okazuje, nawet w prostej pętli można narobić bałaganu) Jeśli masz tablicę, może istnieje prostszy sposób na wyświetlenie wszystkich elementów?
Dla każdej pętli
Począwszy od Java 1.5, programiści Java udostępnili nam projekt
for each loop
opisany na stronie Oracle w Przewodniku o nazwie „
The For-Each Loop ” lub w wersji
1.5.0 . Ogólnie będzie to wyglądać tak:
Możesz przeczytać opis tej konstrukcji w specyfikacji języka Java (JLS), aby upewnić się, że nie jest to magia. Konstrukcja ta jest opisana w rozdziale „
14.14.2. Wzmocniona instrukcja for ”. Jak widać,
pętli for each można używać z tablicami i tablicami, które implementują interfejs
java.lang.Iterable . Oznacza to, że jeśli naprawdę chcesz, możesz zaimplementować interfejs
java.lang.Iterable i
dla każdej pętli można go używać w swojej klasie. Natychmiast powiesz: "OK, to obiekt iterowalny, ale tablica nie jest obiektem. W pewnym sensie. " I będziesz w błędzie, bo... W Javie tablice są dynamicznie tworzonymi obiektami. Specyfikacja języka mówi nam tak: „
W języku programowania Java tablice są obiektami ”. Ogólnie tablice to trochę magia JVM, ponieważ... wewnętrzna struktura tablicy jest nieznana i znajduje się gdzieś wewnątrz wirtualnej maszyny Java. Każdy zainteresowany może przeczytać odpowiedzi na stackoverflow: „
Jak działa klasa tablicowa w Javie? ” Okazuje się, że jeśli nie używamy tablicy, to musimy użyć czegoś, co implementuje
Iterable . Na przykład:
List<String> names = Arrays.asList("John", "Sara", "Jack");
for (String name : names) {
System.out.println("Name = " + name);
}
Tutaj możesz tylko pamiętać, że jeśli użyjemy kolekcji (
java.util.Collection ), dzięki temu otrzymamy dokładnie
Iterable . Jeśli obiekt posiada klasę implementującą Iterable, ma on obowiązek udostępnić przy wywołaniu metody iteratora Iterator, który będzie iterował po zawartości tego obiektu. Na przykład powyższy kod miałby kod bajtowy podobny do tego (w IntelliJ Idea możesz wykonać „Widok” -> „Pokaż kod bajtowy”:
Jak widać, iterator jest faktycznie używany. Gdyby nie
for each pętla , musielibyśmy napisać coś takiego:
List<String> names = Arrays.asList("John", "Sara", "Jack");
for (Iterator i = names.iterator(); i.hasNext(); ) {
String name = (String) i.next();
System.out.println("Name = " + name);
}
Iterator
Jak widzieliśmy powyżej, interfejs
Iterable mówi, że w przypadku instancji jakiegoś obiektu można uzyskać iterator, za pomocą którego można iterować po zawartości. Ponownie można powiedzieć, że jest to zasada pojedynczej odpowiedzialności firmy
SOLID . Sama struktura danych nie powinna sterować przechodzeniem, ale może zapewnić taką, która powinna. Podstawowa implementacja
Iteratora polega na tym, że jest on zwykle deklarowany jako klasa wewnętrzna, która ma dostęp do zawartości klasy zewnętrznej i udostępnia żądany element zawarty w klasie zewnętrznej. Oto przykład z klasy,
ArrayList
w jaki sposób iterator zwraca element:
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
Jak widać za pomocą
ArrayList.this
iteratora uzyskujemy dostęp do klasy zewnętrznej i jej zmiennej
elementData
, a następnie zwracamy stamtąd element. Zatem uzyskanie iteratora jest bardzo proste:
List<String> names = Arrays.asList("John", "Sara", "Jack");
Iterator<String> iterator = names.iterator();
Jej działanie sprowadza się do tego, że możemy sprawdzić, czy znajdują się tam elementy dalej ( metoda
hasNext ), pobrać kolejny element ( metoda
next ) oraz metodą
usuwania , która usuwa ostatni element otrzymany poprzez
next . Metoda
usuwania jest opcjonalna i nie ma gwarancji, że zostanie zaimplementowana. W rzeczywistości wraz z ewolucją Java ewoluują także interfejsy. Dlatego w Javie 8 pojawiła się także metoda
forEachRemaining
pozwalająca na wykonanie jakiejś akcji na pozostałych elementach nieodwiedzonych przez iterator. Co jest interesującego w iteratorze i kolekcjach? Na przykład istnieje klasa
AbstractList
. Jest to klasa abstrakcyjna, która jest rodzicem
ArrayList
i
LinkedList
. A jest to dla nas interesujące ze względu na takie pole jak
modCount . Każda zmiana powoduje zmianę zawartości listy. Więc jakie to ma dla nas znaczenie? Oraz fakt, że iterator pilnuje, aby podczas działania kolekcja, po której jest iterowany, nie uległa zmianie. Jak rozumiesz, implementacja iteratora dla list znajduje się w tym samym miejscu co
modcount , czyli w klasie
AbstractList
. Spójrzmy na prosty przykład:
List<String> names = Arrays.asList("John", "Sara", "Jack");
names = new ArrayList(names);
Iterator<String> iterator = names.iterator();
names.add("modcount++");
System.out.println(iterator.next());
Oto pierwsza ciekawa rzecz, choć nie na temat. Właściwie
Arrays.asList
zwraca swój własny, specjalny
ArrayList
(
java.util.Arrays.ArrayList ). Nie implementuje metod dodawania, więc nie można go modyfikować. Jest o tym napisane w JavaDoc:
fix-size . Ale w rzeczywistości jest to coś więcej niż
stały rozmiar . Jest także
niezmienne , to znaczy niezmienne; usuń również na tym nie zadziała. Otrzymamy również błąd, ponieważ... Po stworzeniu iteratora zapamiętaliśmy w nim
modcount . Następnie zmieniliśmy stan kolekcji „zewnętrznie” (tj. nie poprzez iterator) i wykonaliśmy metodę iteratora. Dlatego pojawia się błąd:
java.util.ConcurrentModificationException . Aby tego uniknąć, zmiany podczas iteracji należy dokonać poprzez sam iterator, a nie poprzez dostęp do kolekcji:
List<String> names = Arrays.asList("John", "Sara", "Jack");
names = new ArrayList(names);
Iterator<String> iterator = names.iterator();
iterator.next();
iterator.remove();
System.out.println(iterator.next());
Jak rozumiesz, jeśli
iterator.remove()
nie zrobisz tego wcześniej
iterator.next()
, to dlatego. iterator nie wskazuje żadnego elementu, wówczas otrzymamy błąd. W przykładzie iterator przejdzie do elementu
John , usunie go, a następnie pobierze element
Sara . I tutaj wszystko byłoby w porządku, ale pech, znowu są „niuanse”)
java.util.ConcurrentModificationException wystąpi tylko wtedy, gdy
hasNext()
zwróci
true . Oznacza to, że jeśli usuniesz ostatni element w samej kolekcji, iterator nie przestanie działać. Po więcej szczegółów warto obejrzeć reportaż o łamigłówkach Java z „
#ITsubbotnik Sekcja JAVA: Zagadki Java ”. Zaczęliśmy tak szczegółową rozmowę z prostego powodu, że dokładnie te same niuanse mają zastosowanie, gdy
for each loop
... Nasz ulubiony iterator jest używany pod maską. I tam również obowiązują wszystkie te niuanse. Tyle, że nie będziemy mieli dostępu do iteratora i nie będziemy mogli bezpiecznie usunąć elementu. Przy okazji, jak rozumiesz, stan jest zapamiętywany w momencie utworzenia iteratora. Bezpieczne usuwanie działa tylko tam, gdzie zostało wywołane. Oznacza to, że ta opcja nie będzie działać:
Iterator<String> iterator1 = names.iterator();
Iterator<String> iterator2 = names.iterator();
iterator1.next();
iterator1.remove();
System.out.println(iterator2.next());
Bo dla iteratora2 usunięcie przez iterator1 było „zewnętrzne”, czyli zostało wykonane gdzieś na zewnątrz i on nic o tym nie wie. Jeśli chodzi o iteratory, również chciałbym to zauważyć. Specjalnie dla implementacji interfejsów powstał specjalny, rozszerzony iterator
List
. I nazwali go
ListIterator
. Pozwala poruszać się nie tylko do przodu, ale także do tyłu, a także pozwala poznać indeks poprzedniego i następnego elementu. Dodatkowo umożliwia podmianę bieżącego elementu lub wstawienie nowego w pozycji pomiędzy bieżącą pozycją iteratora a następną. Jak się domyślasz,
ListIterator
można to zrobić, ponieważ
List
zaimplementowano dostęp przez indeks.
Java 8 i iteracja
Wydanie Java 8 ułatwiło życie wielu osobom. Nie zignorowaliśmy także iteracji po zawartości obiektów. Aby zrozumieć, jak to działa, trzeba powiedzieć o tym kilka słów. W Javie 8 wprowadzono klasę
java.util.function.Consumer . Oto przykład:
Consumer consumer = new Consumer() {
@Override
public void accept(Object o) {
System.out.println(o);
}
};
Konsument jest interfejsem funkcjonalnym, co oznacza, że wewnątrz interfejsu znajduje się tylko 1 niezaimplementowana metoda abstrakcyjna, która wymaga obowiązkowej implementacji w klasach, które określają implementacje tego interfejsu. Pozwala to na użycie tak magicznej rzeczy jak lambda. Ten artykuł nie jest o tym, ale musimy zrozumieć, dlaczego możemy z niego skorzystać. Zatem używając lambd, powyższy
Konsument można przepisać w następujący sposób:
Consumer consumer = (obj) -> System.out.println(obj);
Oznacza to, że Java widzi, że coś zwanego obj zostanie przekazane na wejście, a następnie dla tego obj zostanie wykonane wyrażenie po ->. Jeśli chodzi o iterację, teraz możemy to zrobić:
List<String> names = Arrays.asList("John", "Sara", "Jack");
Consumer consumer = (obj) -> System.out.println(obj);
names.forEach(consumer);
Jeśli przejdziesz do metody
forEach
, zobaczysz, że wszystko jest szalenie proste. Oto nasz ulubiony
for-each loop
:
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
Możliwe jest również piękne usunięcie elementu za pomocą iteratora, na przykład:
List<String> names = Arrays.asList("John", "Sara", "Jack");
names = new ArrayList(names);
Predicate predicate = (obj) -> obj.equals("John");
names.removeIf(predicate);
W tym przypadku metoda
usuwaniaIf przyjmuje jako dane wejściowe nie
Consumer , ale
Predicate . Zwraca
wartość logiczną . W takim przypadku, jeśli predykat powie „
true ”, wówczas element zostanie usunięty. Ciekawe, że tutaj też nie wszystko jest oczywiste)) No cóż, czego chcesz? Na konferencji trzeba zapewnić ludziom przestrzeń do układania puzzli. Weźmy na przykład następujący kod usuwający wszystko, do czego iterator może dotrzeć po pewnej iteracji:
List<String> names = Arrays.asList("John", "Sara", "Jack");
names = new ArrayList(names);
Iterator<String> iterator = names.iterator();
iterator.next();
while (iterator.hasNext()) {
iterator.next();
iterator.remove();
}
System.out.println(names);
OK, tutaj wszystko działa. Ale mimo wszystko pamiętamy tę Java 8. Spróbujmy zatem uprościć kod:
List<String> names = Arrays.asList("John", "Sara", "Jack");
names = new ArrayList(names);
Iterator<String> iterator = names.iterator();
iterator.next();
iterator.forEachRemaining(obj -> iterator.remove());
System.out.println(names);
Czy naprawdę stało się piękniejsze? Wystąpi jednak wyjątek
java.lang.IllegalStateException . A powodem jest... błąd w Javie. Okazuje się, że jest to naprawione, ale w JDK 9. Oto link do zadania w OpenJDK:
Iterator.forEachRemaining vs. Iterator.usuń . Oczywiście zostało to już omówione:
Dlaczego iterator.forEachRemaining nie usuwa elementu z lambdy konsumenta? Cóż, innym sposobem jest bezpośrednio poprzez Stream API:
List<String> names = new ArrayList(Arrays.asList("John", "Sara", "Jack"));
Stream<String> stream = names.stream();
stream.forEach(obj -> System.out.println(obj));
wnioski
Jak widzieliśmy z całego powyższego materiału, pętla
for-each loop
to po prostu „cukier składniowy” na wierzchu iteratora. Jednak obecnie jest on stosowany w wielu miejscach. Ponadto każdy produkt należy stosować ostrożnie. Na przykład nieszkodliwy
forEachRemaining
może kryć nieprzyjemne niespodzianki. A to po raz kolejny dowodzi, że testy jednostkowe są potrzebne. Dobry test może zidentyfikować taki przypadek użycia w kodzie. Co możesz obejrzeć/przeczytać na ten temat:
#Wiaczesław
GO TO FULL VERSION