JavaRush /Java-Blog /Random-DE /For- und For-Each-Schleife: eine Geschichte darüber, wie ...
Viacheslav
Level 3

For- und For-Each-Schleife: eine Geschichte darüber, wie ich iteriert habe, iteriert wurde, aber nicht iteriert wurde

Veröffentlicht in der Gruppe Random-DE

Einführung

Schleifen gehören zu den Grundstrukturen von Programmiersprachen. Auf der Oracle-Website gibt es beispielsweise einen Abschnitt „ Lektion: Sprachgrundlagen “, in dem Schleifen eine separate Lektion „ Die for-Anweisung “ haben. Frischen wir die Grundlagen auf: Die Schleife besteht aus drei Ausdrücken (Anweisungen): Initialisierung (Initialisierung), Bedingung (Beendigung) und Inkrement (Inkrement):
For- und For-Each-Schleife: eine Geschichte darüber, wie ich iterierte, iterierte, aber nicht iterierte – 1
Interessanterweise sind sie alle optional, was bedeutet, dass wir, wenn wir möchten, schreiben können:
for (;;){
}
In diesem Fall erhalten wir zwar eine Endlosschleife, weil Wir geben keine Bedingung für das Verlassen der Schleife (Abbruch) an. Der Initialisierungsausdruck wird nur einmal ausgeführt, bevor die gesamte Schleife ausgeführt wird. Es ist immer wichtig, sich daran zu erinnern, dass ein Zyklus seinen eigenen Umfang hat. Dies bedeutet, dass Initialisierung , Beendigung , Inkrement und der Schleifenkörper dieselben Variablen sehen. Der Umfang lässt sich immer einfach anhand der geschweiften Klammern ermitteln. Alles innerhalb der Klammern ist außerhalb der Klammern nicht sichtbar, aber alles außerhalb der Klammern ist innerhalb der Klammern sichtbar. Initialisierung ist nur ein Ausdruck. Anstatt beispielsweise eine Variable zu initialisieren, können Sie im Allgemeinen eine Methode aufrufen, die nichts zurückgibt. Oder überspringen Sie es einfach und lassen Sie vor dem ersten Semikolon ein Leerzeichen. Der folgende Ausdruck gibt die Beendigungsbedingung an . Solange es true ist , wird die Schleife ausgeführt. Und wenn false , wird keine neue Iteration gestartet. Wenn Sie sich das Bild unten ansehen, erhalten wir beim Kompilieren eine Fehlermeldung und die IDE beschwert sich: Unser Ausdruck in der Schleife ist nicht erreichbar. Da wir keine einzige Iteration in der Schleife haben, werden wir sie sofort verlassen, weil FALSCH:
For- und For-Each-Schleife: eine Geschichte darüber, wie ich iterierte, iterierte, aber nicht iterierte – 2
Es lohnt sich , den Ausdruck in der Beendigungsanweisung im Auge zu behalten : Er bestimmt direkt, ob Ihre Anwendung Endlosschleifen haben wird. Inkrement ist der einfachste Ausdruck. Es wird nach jeder erfolgreichen Iteration der Schleife ausgeführt. Und dieser Ausdruck kann auch übersprungen werden. Zum Beispiel:
int outerVar = 0;
for (;outerVar < 10;) {
	outerVar += 2;
	System.out.println("Value = " + outerVar);
}
Wie Sie dem Beispiel entnehmen können, werden wir bei jeder Iteration der Schleife in Schritten von 2 inkrementieren, jedoch nur solange der Wert outerVarkleiner als 10 ist. Da der Ausdruck in der Inkrementanweisung außerdem eigentlich nur ein Ausdruck ist, wird er kann alles enthalten. Daher verbietet niemand die Verwendung einer Dekrementierung anstelle einer Inkrementierung, d.h. Wert mindern. Sie sollten das Schreiben des Inkrements immer überwachen. +=führt zuerst eine Erhöhung und dann eine Zuweisung durch, aber wenn wir im obigen Beispiel das Gegenteil schreiben, erhalten wir eine Endlosschleife, da die Variable outerVarniemals den geänderten Wert erhält: In diesem Fall =+wird er nach der Zuweisung berechnet. Das Gleiche gilt übrigens auch für die Ansichtsinkremente ++. Wir hatten zum Beispiel eine Schleife:
String[] names = {"John","Sara","Jack"};
for (int i = 0; i < names.length; ++i) {
	System.out.println(names[i]);
}
Der Zyklus funktionierte und es gab keine Probleme. Doch dann kam der Refactoring-Mann. Er verstand die Erhöhung nicht und tat einfach Folgendes:
String[] names = {"John","Sara","Jack"};
for (int i = 0; i < names.length;) {
	System.out.println(names[++i]);
}
Wenn das Inkrementierungszeichen vor dem Wert erscheint, bedeutet dies, dass dieser zunächst erhöht wird und dann an die Stelle zurückkehrt, an der er angezeigt wird. In diesem Beispiel beginnen wir sofort mit dem Extrahieren des Elements am Index 1 aus dem Array und überspringen das erste. Und dann stürzen wir bei Index 3 mit dem Fehler „ java.lang.ArrayIndexOutOfBoundsException “ ab. Wie Sie vielleicht vermutet haben, funktionierte dies früher einfach deshalb, weil das Inkrement nach Abschluss der Iteration aufgerufen wird. Bei der Übertragung dieses Ausdrucks auf die Iteration ist alles kaputt gegangen. Wie sich herausstellt, kann selbst in einer einfachen Schleife ein Chaos entstehen.) Wenn Sie ein Array haben, gibt es vielleicht eine einfachere Möglichkeit, alle Elemente anzuzeigen?
For- und For-Each-Schleife: eine Geschichte darüber, wie ich iterierte, iterierte, aber nicht iterierte – 3

Für jede Schleife

Beginnend mit Java 1.5 stellten uns die Java-Entwickler ein Design zur Verfügung, das for each loopauf der Oracle-Website im Handbuch mit dem Namen „ The For-Each Loop “ oder für Version 1.5.0 beschrieben wurde . Im Allgemeinen wird es so aussehen:
For- und For-Each-Schleife: eine Geschichte darüber, wie ich iterierte, iterierte, aber nicht iterierte – 4
Sie können die Beschreibung dieses Konstrukts in der Java Language Specification (JLS) lesen, um sicherzustellen, dass es sich nicht um Zauberei handelt. Diese Konstruktion wird im Kapitel „ 14.14.2. Die erweiterte for-Anweisung “ beschrieben. Wie Sie sehen, kann die for every-Schleife mit Arrays und solchen verwendet werden, die die java.lang.Iterable- Schnittstelle implementieren . Das heißt, wenn Sie wirklich möchten, können Sie die Schnittstelle java.lang.Iterable implementieren und für jede Schleife mit Ihrer Klasse verwenden. Sie werden sofort sagen: „Okay, es ist ein iterierbares Objekt, aber ein Array ist kein Objekt. Irgendwie.“ Und Sie werden sich irren, denn... In Java sind Arrays dynamisch erstellte Objekte. Die Sprachspezifikation sagt uns Folgendes: „ In der Programmiersprache Java sind Arrays Objekte .“ Im Allgemeinen sind Arrays eine Art JVM-Magie, weil ... Wie das Array intern aufgebaut ist, ist unbekannt und befindet sich irgendwo innerhalb der Java Virtual Machine. Jeder Interessierte kann die Antworten auf Stackoverflow lesen: „ Wie funktioniert die Array-Klasse in Java? “ Es stellt sich heraus, dass wir, wenn wir kein Array verwenden, etwas verwenden müssen, das Iterable implementiert . Zum Beispiel:
List<String> names = Arrays.asList("John", "Sara", "Jack");
for (String name : names) {
	System.out.println("Name = " + name);
}
Hier können Sie sich nur daran erinnern, dass wir genau Iterable erhalten, wenn wir Sammlungen ( java.util.Collection ) verwenden . Wenn ein Objekt über eine Klasse verfügt, die Iterable implementiert, ist es verpflichtet, beim Aufruf der Iterator-Methode einen Iterator bereitzustellen, der den Inhalt dieses Objekts iteriert. Der obige Code hätte beispielsweise einen Bytecode wie diesen (in IntelliJ Idea können Sie „Ansicht“ -> „Bytecode anzeigen“ ausführen:
For- und For-Each-Schleife: eine Geschichte darüber, wie ich iterierte, iterierte, aber nicht iterierte – 5
Wie Sie sehen, wird tatsächlich ein Iterator verwendet. Ohne die for every-Schleife müssten wir so etwas schreiben:
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

Wie wir oben gesehen haben, besagt die Iterable -Schnittstelle , dass Sie für Instanzen eines Objekts einen Iterator erhalten können, mit dem Sie über den Inhalt iterieren können. Auch dies kann man als das Single-Responsibility-Prinzip von SOLID bezeichnen . Die Datenstruktur selbst sollte die Durchquerung nicht steuern, sie kann jedoch eine solche bereitstellen. Die grundlegende Implementierung von Iterator besteht darin, dass er normalerweise als innere Klasse deklariert wird, die Zugriff auf den Inhalt der äußeren Klasse hat und das gewünschte in der äußeren Klasse enthaltene Element bereitstellt. Hier ist ein Beispiel aus der Klasse ArrayList, wie ein Iterator ein Element zurückgibt:
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];
}
Wie wir sehen können, greift ArrayList.thisein Iterator auf die äußere Klasse und ihre Variable zu elementDataund gibt dann von dort ein Element zurück. Es ist also ganz einfach, einen Iterator zu bekommen:
List<String> names = Arrays.asList("John", "Sara", "Jack");
Iterator<String> iterator = names.iterator();
Seine Arbeit läuft darauf hinaus, dass wir prüfen können, ob weitere Elemente vorhanden sind (die hasNext- Methode ), das nächste Element abrufen (die next- Methode ) und die Remove- Methode verwenden können, die das letzte über next empfangene Element entfernt . Die Methode „remove“ ist optional und es kann nicht garantiert werden, dass sie implementiert wird. Tatsächlich entwickeln sich mit der Weiterentwicklung von Java auch die Schnittstellen weiter. Daher gab es in Java 8 auch eine Methode forEachRemaining, mit der Sie einige Aktionen für die verbleibenden Elemente ausführen konnten, die vom Iterator nicht besucht wurden. Was ist an einem Iterator und Sammlungen interessant? Es gibt zum Beispiel eine Klasse AbstractList. Dies ist eine abstrakte Klasse, die das übergeordnete Element von ArrayListund ist LinkedList. Und es ist für uns wegen eines Feldes wie modCount interessant . Bei jeder Änderung ändert sich der Inhalt der Liste. Was bedeutet uns das also? Und die Tatsache, dass der Iterator sicherstellt, dass sich die Sammlung, über die iteriert wird, während des Betriebs nicht ändert. Wie Sie wissen, befindet sich die Implementierung des Iterators für Listen an derselben Stelle wie modcount , also in der Klasse AbstractList. Schauen wir uns ein einfaches Beispiel an:
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());
Hier ist die erste interessante Sache, wenn auch nicht zum Thema. Gibt tatsächlich Arrays.asListeine eigene spezielle zurück ArrayList( java.util.Arrays.ArrayList ). Es implementiert keine Methoden zum Hinzufügen von Methoden und kann daher nicht geändert werden. Darüber wird im JavaDoc geschrieben: Fixed-Size . Aber tatsächlich ist es mehr als nur eine feste Größe . Es ist auch unveränderlich , das heißt unveränderlich; Remove wird damit auch nicht funktionieren. Wir werden auch eine Fehlermeldung erhalten, weil... Nachdem wir den Iterator erstellt hatten, erinnerten wir uns an den darin enthaltenen Modcount . Dann haben wir den Status der Sammlung „extern“ (d. h. nicht über den Iterator) geändert und die Iterator-Methode ausgeführt. Daher erhalten wir den Fehler: java.util.ConcurrentModificationException . Um dies zu vermeiden, muss die Änderung während der Iteration über den Iterator selbst und nicht über den Zugriff auf die Sammlung erfolgen:
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());
Wie Sie verstehen, wenn iterator.remove()Sie es nicht vorher tun iterator.next(), dann weil. Zeigt der Iterator auf kein Element, erhalten wir eine Fehlermeldung. Im Beispiel geht der Iterator zum John- Element , entfernt es und ruft dann das Sara- Element ab . Und hier wäre alles in Ordnung, aber Pech gehabt, es gibt wieder „Nuancen“). java.util.ConcurrentModificationException tritt nur auf, wenn es truehasNext() zurückgibt . Das heißt, wenn Sie das letzte Element über die Sammlung selbst löschen, stürzt der Iterator nicht ab. Für weitere Details schauen Sie sich am besten den Bericht über Java-Rätsel von „ #ITsubbotnik Section JAVA: Java-Rätsel “ an. Wir haben ein so ausführliches Gespräch aus dem einfachen Grund begonnen, weil genau die gleichen Nuancen gelten, wenn ... Unser Lieblings-Iterator wird unter der Haube verwendet. Und all diese Nuancen gelten auch dort. Das Einzige ist, dass wir keinen Zugriff auf den Iterator haben und das Element nicht sicher entfernen können. Wie Sie wissen, wird übrigens der Zustand in dem Moment gespeichert, in dem der Iterator erstellt wird. Und das sichere Löschen funktioniert nur dort, wo es aufgerufen wird. Das heißt, diese Option funktioniert nicht: for each loop
Iterator<String> iterator1 = names.iterator();
Iterator<String> iterator2 = names.iterator();
iterator1.next();
iterator1.remove();
System.out.println(iterator2.next());
Denn für Iterator2 war das Löschen durch Iterator1 „extern“, das heißt, es wurde irgendwo außerhalb durchgeführt und er weiß nichts davon. Zum Thema Iteratoren möchte ich auch Folgendes anmerken. Speziell für Schnittstellenimplementierungen wurde ein spezieller, erweiterter Iterator erstellt List. Und sie gaben ihm den Namen ListIterator. Es ermöglicht Ihnen, sich nicht nur vorwärts, sondern auch rückwärts zu bewegen und den Index des vorherigen und des nächsten Elements herauszufinden. Darüber hinaus können Sie das aktuelle Element ersetzen oder ein neues an einer Position zwischen der aktuellen Iteratorposition und der nächsten einfügen. Wie Sie vermutet haben, ListIteratorist dies zulässig, da Listder Zugriff über den Index implementiert ist.
For- und For-Each-Schleife: eine Geschichte darüber, wie ich iterierte, iterierte, aber nicht iterierte – 6

Java 8 und Iteration

Die Veröffentlichung von Java 8 hat vielen das Leben erleichtert. Wir haben auch die Iteration über den Inhalt von Objekten nicht ignoriert. Um zu verstehen, wie das funktioniert, müssen Sie ein paar Worte dazu sagen. Mit Java 8 wurde die Klasse java.util.function.Consumer eingeführt . Hier ist ein Beispiel:
Consumer consumer = new Consumer() {
	@Override
	public void accept(Object o) {
		System.out.println(o);
	}
};
Consumer ist eine funktionale Schnittstelle, was bedeutet, dass es innerhalb der Schnittstelle nur eine nicht implementierte abstrakte Methode gibt, die eine obligatorische Implementierung in den Klassen erfordert, die die Implementierungen dieser Schnittstelle angeben. Dadurch können Sie so etwas Magisches wie Lambda verwenden. In diesem Artikel geht es nicht darum, aber wir müssen verstehen, warum wir es verwenden können. Mit Lambdas kann der obige Consumer also wie folgt umgeschrieben werden: Consumer consumer = (obj) -> System.out.println(obj); Dies bedeutet, dass Java sieht, dass etwas namens obj an die Eingabe übergeben wird, und dann wird der Ausdruck nach -> für dieses obj ausgeführt. Was die Iteration betrifft, können wir jetzt Folgendes tun:
List<String> names = Arrays.asList("John", "Sara", "Jack");
Consumer consumer = (obj) -> System.out.println(obj);
names.forEach(consumer);
Wenn Sie zur Methode gehen forEach, werden Sie sehen, dass alles verrückt einfach ist. Hier ist unser Favorit for-each loop:
default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
}
Es ist auch möglich, ein Element mithilfe eines Iterators auf schöne Weise zu entfernen, zum Beispiel:
List<String> names = Arrays.asList("John", "Sara", "Jack");
names = new ArrayList(names);
Predicate predicate = (obj) -> obj.equals("John");
names.removeIf(predicate);
In diesem Fall nimmt die Methode „removeIf“ als Eingabe nicht „Consumer“ , sondern „Predicate“ an . Es gibt boolean zurück . Wenn das Prädikat in diesem Fall „ true “ lautet, wird das Element entfernt. Interessant ist, dass auch hier nicht alles offensichtlich ist)) Na ja, was willst du? Den Menschen muss auf der Konferenz Raum gegeben werden, um Rätsel zu lösen. Nehmen wir zum Beispiel den folgenden Code zum Löschen aller Dinge, die der Iterator nach einer Iteration erreichen kann:
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(); // Удалoder его
}
System.out.println(names);
Okay, hier funktioniert alles. Aber wir erinnern uns immerhin an Java 8. Versuchen wir daher, den Code zu vereinfachen:
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);
Ist es wirklich schöner geworden? Es wird jedoch eine java.lang.IllegalStateException geben . Und der Grund ist ... ein Fehler in Java. Es stellt sich heraus, dass es behoben ist, aber in JDK 9. Hier ist ein Link zur Aufgabe in OpenJDK: Iterator.forEachRemaining vs. Iterator.remove . Natürlich wurde dies bereits besprochen: Warum entfernt iterator.forEachRemaining kein Element im Consumer-Lambda? Nun, ein anderer Weg ist direkt über die Stream-API:
List<String> names = new ArrayList(Arrays.asList("John", "Sara", "Jack"));
Stream<String> stream = names.stream();
stream.forEach(obj -> System.out.println(obj));

Schlussfolgerungen

Wie wir aus dem gesamten Material oben gesehen haben, for-each loopist eine Schleife nur „syntaktischer Zucker“ über einem Iterator. Mittlerweile wird es jedoch vielerorts verwendet. Darüber hinaus sollte jedes Produkt mit Vorsicht verwendet werden. Beispielsweise forEachRemainingkann ein harmloses Motiv unangenehme Überraschungen verbergen. Und dies beweist einmal mehr, dass Unit-Tests erforderlich sind. Ein guter Test könnte einen solchen Anwendungsfall in Ihrem Code identifizieren. Was Sie zum Thema sehen/lesen können: #Wjatscheslaw
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION