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):
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:
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
outerVar
kleiner 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
outerVar
niemals 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?
Für jede Schleife
Beginnend mit Java 1.5 stellten uns die Java-Entwickler ein Design zur Verfügung, das
for each loop
auf 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:
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:
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(); i.hasNext(); ) {
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.this
ein Iterator auf die äußere Klasse und ihre Variable zu
elementData
und 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
ArrayList
und 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.asList
eine 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,
ListIterator
ist dies zulässig, da
List
der Zugriff über den Index implementiert ist.
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();
while (iterator.hasNext()) {
iterator.next();
iterator.remove();
}
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();
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 loop
ist 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
forEachRemaining
kann 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
GO TO FULL VERSION