547 Stimmen

Java 8 Iterable.forEach() vs foreach-Schleife

Welche der folgenden Praktiken ist in Java 8 besser?

Java 8:

joins.forEach(join -> mIrc.join(mSession, join));

Java 7:

for (String join : joins) {
    mIrc.join(mSession, join);
}

Ich habe viele for-Schleifen, die mit Lambdas "vereinfacht" werden könnten, aber gibt es wirklich einen Vorteil, sie zu verwenden? Würde es ihre Leistung und Lesbarkeit verbessern?

EDIT

Ich werde diese Frage auch auf längere Methoden ausdehnen. Ich weiß, dass man von einer Lambda-Funktion aus die übergeordnete Funktion nicht zurückgeben oder beenden kann und dies sollte ebenfalls berücksichtigt werden, wenn man sie vergleicht, aber gibt es noch etwas anderes zu beachten?

632voto

Aleksandr Dubinsky Punkte 20646

Die bessere Praxis ist die Verwendung von for-each. Neben der Verletzung des Prinzips Keep It Simple, Stupid hat das neuartige forEach() mindestens folgende Mängel:

  • Nicht-veränderliche Variablen können nicht verwendet werden. Daher kann Code wie der folgende nicht in eine forEach-Lambda umgewandelt werden:
Object prev = null;
for(Object curr : list)
{
    if( prev != null )
        foo(prev, curr);
    prev = curr;
}
  • Behandlung von überprüften Ausnahmen nicht möglich. Lambdas dürfen zwar überprüfte Ausnahmen werfen, aber gängige funktionale Schnittstellen wie Consumer deklarieren keine. Daher muss Code, der überprüfte Ausnahmen wirft, diese in try-catch oder Throwables.propagate() einwickeln. Auch wenn Sie das tun, ist nicht immer klar, was mit der geworfenen Ausnahme passiert. Sie könnte irgendwo im Inneren von forEach() verschluckt werden.

  • Eingeschränkte Flusssteuerung. Ein return in einer Lambda entspricht einem continue in einem for-each, aber es gibt kein Äquivalent zu einem break. Es ist auch schwierig, Dinge wie Rückgabewerte, Kurzschlüsse oder Flags setzen zu tun (was die Dinge etwas erleichtert hätte, wenn es keine Verletzung der Regel keine nicht-finalen Variablen gewesen wäre). "Dies ist nicht nur eine Optimierung, sondern entscheidend, wenn Sie bedenken, dass einige Sequenzen (z. B. das Lesen der Zeilen in einer Datei) Nebenwirkungen haben können oder Sie eine unendliche Sequenz haben könnten."

  • Könnte parallel ausgeführt werden, was für alles außer den 0,1% Ihres Codes, der optimiert werden muss, schrecklich ist. Jeder parallele Code muss durchdacht sein (auch wenn er keine Sperren, Volatilen und andere besonders hässliche Aspekte der traditionellen mehrfadigen Ausführung verwendet). Jeder Fehler wird schwer zu finden sein.

  • Könnte die Leistung beeinträchtigen, weil der JIT forEach()+Lambda nicht in dem Maße optimieren kann wie einfache Schleifen, insbesondere jetzt, da Lambdas neu sind. Mit "Optimierung" meine ich nicht den Overhead des Aufrufs von Lambdas (der gering ist), sondern die ausgefeilte Analyse und Transformation, die der moderne JIT-Compiler beim Ausführen des Codes durchführt.

  • Wenn Sie Parallelität benötigen, ist es wahrscheinlich einfacher und nicht viel schwieriger, einen ExecutorService zu verwenden. Streams sind sowohl automagisch (lesen: wissen nicht viel über Ihr Problem) als auch verwenden eine spezialisierte (lesen: ineffiziente für den allgemeinen Fall) Parallelisierungsstrategie (Gabel-Zusammenführung rekursive Zerlegung).

  • Erschwert das Debuggen, aufgrund der verschachtelten Aufrufhierarchie und, Gott bewahre, paralleler Ausführung. Der Debugger kann Probleme haben, Variablen aus dem umgebenden Code anzuzeigen, und Dinge wie Schritt-für-Schritt könnten nicht wie erwartet funktionieren.

  • Streams im Allgemeinen sind schwieriger zu codieren, zu lesen und zu debuggen. Das gilt eigentlich für komplexe "fließende" APIs im Allgemeinen. Die Kombination aus komplexen Einzelanweisungen, häufigem Einsatz von Generika und Mangel an Zwischenvariablen führt zu verwirrenden Fehlermeldungen und frustriert das Debuggen. Anstatt "diese Methode hat keine Überladung für Typ X" zu erhalten, erhalten Sie eine Fehlermeldung, die näher daran ist, "irgendwo haben Sie die Typen durcheinander gebracht, aber wir wissen nicht wo oder wie". Ebenso können Sie nicht so einfach durch den Code hindurchgehen und Dinge im Debugger untersuchen, wenn der Code in mehrere Anweisungen aufgeteilt ist und Zwischenwerte in Variablen gespeichert sind. Schließlich kann das Lesen des Codes und das Verständnis der Typen und des Verhaltens in jeder Ausführungsstufe nicht trivial sein.

  • Sticht wie ein schmerzender Daumen heraus. Die Java-Sprache hat bereits die for-each-Anweisung. Warum ersetzen Sie sie durch einen Funktionsaufruf? Warum verstecken Sie Nebeneffekte irgendwo in Ausdrücken? Warum ermutigen Sie klobige Einzeiler? Das beliebige Vermischen von regulären for-each und neuen forEach ist schlechter Stil. Code sollte in Idiomen sprechen (Muster, die aufgrund ihrer Wiederholung schnell verständlich sind), und je weniger Idiome verwendet werden, desto klarer ist der Code und desto weniger Zeit wird damit verbracht, zu entscheiden, welches Idiom verwendet werden soll (ein großes Zeitproblem für Perfektionisten wie mich!).

Wie Sie sehen können, bin ich kein großer Fan von forEach(), außer in Fällen, in denen es sinnvoll ist.

Besonders ärgerlich für mich ist die Tatsache, dass Stream nicht Iterable implementiert (obwohl die Methode iterator tatsächlich vorhanden ist) und nicht mit einem for-each, sondern nur mit einem forEach() verwendet werden kann. Ich empfehle, Streams in Iterables mit (Iterable)stream::iterator umzuwandeln. Eine bessere Alternative ist die Verwendung von StreamEx, das eine Reihe von Problemen mit der Stream-API löst, einschließlich der Implementierung von Iterable.

Das gesagt, ist forEach() nützlich für Folgendes:

  • Atomares Iterieren über eine synchronisierte Liste. Vorher war eine mit Collections.synchronizedList() generierte Liste zwar atomar in Bezug auf Dinge wie get oder set, aber beim Iterieren nicht threadsicher.

  • Parallele Ausführung (unter Verwendung eines entsprechenden parallelen Streams). Dies spart Ihnen einige Zeilen Code im Vergleich zur Verwendung eines ExecutorService, wenn Ihr Problem mit den in Streams und Spliteratoren eingebauten Leistungsannahmen übereinstimmt.

  • Bestimmte Container, die, wie die synchronisierte Liste, davon profitieren, die Kontrolle über die Iteration zu haben (obwohl dies größtenteils theoretisch ist, es sei denn, es gibt mehr Beispiele).

  • Aufrufen einer einzigen Funktion sauberer, indem Sie forEach() und einen Methodenreferenzargument verwenden (d. h. list.forEach (obj::someMethod)). Beachten Sie jedoch die Punkte zu überprüften Ausnahmen, schwierigerem Debuggen und zur Reduzierung der Anzahl von Idiomen, die Sie beim Schreiben von Code verwenden.

Artikel, die ich als Referenz benutzt habe:

EDIT: Es sieht so aus, als ob einige der ursprünglichen Vorschläge für Lambdas (wie http://www.javac.info/closures-v06a.html Google Cache) einige der von mir erwähnten Probleme gelöst haben (während sie natürlich ihre eigenen Komplikationen hinzugefügt haben).

174voto

mschenk74 Punkte 3551

Der Vorteil wird deutlich, wenn die Operationen parallel ausgeführt werden können. (Siehe http://java.dzone.com/articles/devoxx-2012-java-8-lambda-and - der Abschnitt über interne und externe Iteration)

  • Der Hauptvorteil aus meiner Sicht ist, dass die Implementierung dessen, was innerhalb der Schleife ausgeführt werden soll, definiert werden kann, ohne entscheiden zu müssen, ob es parallel oder sequentiell ausgeführt wird

  • Wenn Sie möchten, dass Ihre Schleife parallel ausgeführt wird, könnten Sie einfach folgendes schreiben

    joins.parallelStream().forEach(join -> mIrc.join(mSession, join));

    Sie müssen etwas zusätzlichen Code für die Thread-Verwaltung usw. schreiben.

Hinweis: Für meine Antwort habe ich angenommen, dass joins das java.util.Stream-Interface implementiert. Wenn joins nur das java.util.Iterable-Interface implementiert, trifft das nicht mehr zu.

13voto

ZhongYu Punkte 18778

forEach() kann implementiert werden, um schneller zu sein als die for-each-Schleife, weil das Iterable am besten weiß, wie es seine Elemente durchlaufen kann, im Gegensatz zur Standard-Iterator-Methode. Also besteht der Unterschied darin, ob intern oder extern eine Schleife durchlaufen wird.

Beispielsweise kann ArrayList.forEach(action) einfach implementiert werden als

for(int i=0; i

``

im Gegensatz zur for-each-Schleife, die viel Gerüst benötigt

Iterator iter = list.iterator();
while(iter.hasNext())
    Object next = iter.next();
    mach etwas mit `next`

Allerdings müssen wir auch zwei zusätzliche Kosten berücksichtigen, wenn forEach() verwendet wird, einerseits das Erstellen des Lambda-Objekts, andererseits das Aufrufen der Lambda-Methode. Diese sind wahrscheinlich nicht signifikant.

Siehe auch http://journal.stuffwithstuff.com/2013/01/13/iteration-inside-and-out/ für einen Vergleich von internen/externen Iterationen in verschiedenen Anwendungsfällen.

``

10voto

Assaf Punkte 1334

TL;DR: List.stream().forEach() war am schnellsten.

Ich hatte das Gefühl, meine Ergebnisse aus dem Benchmarking von Iterationen hinzufügen zu sollten. Ich habe einen sehr einfachen Ansatz gewählt (keine Benchmarking-Frameworks) und 5 verschiedene Methoden benchmarked:

  1. klassisches for
  2. klassisches foreach
  3. List.forEach()
  4. List.stream().forEach()
  5. List.parallelStream().forEach

das Testverfahren und die Parameter

private List list;
private final int size = 1_000_000;

public MyClass(){
    list = new ArrayList<>();
    Random rand = new Random();
    for (int i = 0; i < size; ++i) {
        list.add(rand.nextInt(size * 50));
    }    
}
private void doIt(Integer i) {
    i *= 2; //damit es nicht JITed wird
}

Die Liste in dieser Klasse soll durchlaufen werden und auf alle ihre Elemente soll doIt(Integer i) angewendet werden, jedes Mal über eine andere Methode. Im Hauptklasse führe ich die getestete Methode dreimal aus, um den JVM aufzuwärmen. Dann führe ich die Testmethode 1000 Mal aus und summiere die Zeit für jede Iterationsmethode (unter Verwendung von System.nanoTime()). Danach teile ich diese Summe durch 1000 und das ist das Ergebnis, die durchschnittliche Zeit. Beispiel:

myClass.fored();
myClass.fored();
myClass.fored();
for (int i = 0; i < reps; ++i) {
    begin = System.nanoTime();
    myClass.fored();
    end = System.nanoTime();
    nanoSum += end - begin;
}
System.out.println(nanoSum / reps);

Ich habe dies auf einem i5 4-Kern-CPU mit Java-Version 1.8.0_05 ausgeführt.

klassisches for

for(int i = 0, l = list.size(); i < l; ++i) {
    doIt(list.get(i));
}

Ausführungszeit: 4,21 ms

klassisches foreach

for(Integer i : list) {
    doIt(i);
}

Ausführungszeit: 5,95 ms

List.forEach()

list.forEach((i) -> doIt(i));

Ausführungszeit: 3,11 ms

List.stream().forEach()

list.stream().forEach((i) -> doIt(i));

Ausführungszeit: 2,79 ms

List.parallelStream().forEach

list.parallelStream().forEach((i) -> doIt(i));

Ausführungszeit: 3,6 ms

9voto

Eugene Loy Punkte 12140

Ich fühle, dass ich meinen Kommentar etwas erweitern muss...

Über Paradigma\Stil

Das ist wahrscheinlich das bemerkenswerteste Merkmal. FP wurde aufgrund dessen populär, was Sie erreichen können, wenn Sie Seiteneffekte vermeiden. Ich werde nicht tiefer auf die Vor- und Nachteile eingehen, die Sie daraus ziehen können, da dies nicht mit der Frage zusammenhängt.

Ich werde jedoch sagen, dass die Iteration mit Iterable.forEach von FP inspiriert ist und eher das Ergebnis davon ist, mehr FP nach Java zu bringen (ironischerweise würde ich sagen, dass es in reinem FP nicht viel Verwendung für forEach gibt, da es nichts tut außer Seiteneffekte einzuführen).

Letztendlich würde ich sagen, dass es eher eine Frage des Geschmacks\Stils\Paradigmas ist, in dem Sie gerade schreiben.

Über Parallelismus.

Von der Leistungsperspektive aus gibt es keine versprochenen deutlichen Vorteile bei der Verwendung von Iterable.forEach gegenüber foreach(...).

Laut offizieller Dokumentation zu Iterable.forEach:

Führt die gegebene Aktion auf den Inhalten von Iterable aus, in der Reihenfolge, in der die Elemente auftreten, während der Iteration, bis alle Elemente verarbeitet wurden oder die Aktion eine Ausnahme auslöst.

... d.h. die Dokumentation zeigt ziemlich klar, dass es keine implizite Parallelität geben wird. Eine solche hinzuzufügen wäre ein LSP-Verstoß.

Es gibt jetzt "parallele Sammlungen", die in Java 8 versprochen sind, aber um mit diesen zu arbeiten, müssen Sie expliziter sein und etwas mehr darauf achten, sie zu verwenden (siehe Antwort von mschenk74 zum Beispiel).

Übrigens: in diesem Fall wird Stream.forEach verwendet, und es garantiert nicht, dass die tatsächliche Arbeit parallel erledigt wird (abhängig von der zugrunde liegenden Sammlung).

UPDATE: es ist vielleicht nicht so offensichtlich und auf den ersten Blick etwas übertrieben, aber es gibt noch eine andere Seite des Stil- und Lesbarkeitsaspekts.

Zunächst einmal - einfache alte for-Schleifen sind einfach und alt. Jeder kennt sie bereits.

Zweitens, und wichtiger - Sie möchten Iterable.forEach wahrscheinlich nur mit Einzeilern verwenden. Wenn der "Körper" schwerer wird, neigen sie dazu, nicht so leicht lesbar zu sein. Von hier aus haben Sie 2 Optionen - innere Klassen verwenden (bäh) oder einfache alte for-Schleifen verwenden. Menschen werden oft verärgert, wenn sie dieselben Dinge (Iterationen über Sammlungen) auf verschiedene Arten/Weisen im selben Code sehen, und dies scheint der Fall zu sein.

Noch einmal, dies kann ein Problem sein oder auch nicht. Es hängt von den Personen ab, die am Code arbeiten.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X