JavaRush /Blog Java /Random-PL /Pętla For i For-Each: opowieść o tym, jak wykonałem itera...
Viacheslav
Poziom 3

Pętla For i For-Each: opowieść o tym, jak wykonałem iterację, została powtórzona, ale nie została powtórzona

Opublikowano w grupie Random-PL

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):
Pętla For i For-Each: opowieść o tym, jak iterowałem, iterowałem, ale nie iterowałem - 1
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:
Pętla For i For-Each: opowieść o tym, jak iterowałem, iterowałem, ale nie iterowałem - 2
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ść outerVarbę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 outerVarnigdy 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?
Pętla For i For-Each: opowieść o tym, jak iterowałem, iterowałem, ale nie iterowałem - 3

Dla każdej pętli

Począwszy od Java 1.5, programiści Java udostępnili nam projekt for each loopopisany 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:
Pętla For i For-Each: opowieść o tym, jak iterowałem, iterowałem, ale nie iterowałem - 4
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”:
Pętla For i For-Each: opowieść o tym, jak iterowałem, iterowałem, ale nie iterowałem - 5
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(); /* continue if */ i.hasNext(); /* skip increment */) {
	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, ArrayListw 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.thisiteratora 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 forEachRemainingpozwalają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 ArrayListi 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.asListzwraca 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, ListIteratormożna to zrobić, ponieważ Listzaimplementowano dostęp przez indeks.
Pętla For i For-Each: opowieść o tym, jak iterowałem, iterowałem, ale nie iterowałem - 6

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(); // Курсор на John
while (iterator.hasNext()) {
    iterator.next(); // Следующий элемент
    iterator.remove(); // УдалLub его
}
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(); // Курсор на John
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 loopto 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 forEachRemainingmoż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
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION