Java 8 Iterable.forEach () vs foreach-Schleife

465

Welche der folgenden Methoden ist in Java 8 besser geeignet?

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?

BEARBEITEN

Ich werde diese Frage auch auf längere Methoden ausweiten. Ich weiß, dass Sie die übergeordnete Funktion eines Lambda nicht zurückgeben oder aufheben können, und dies sollte auch beim Vergleich berücksichtigt werden. Aber gibt es noch etwas zu beachten?

nebkat
quelle
10
Es gibt keinen wirklichen Leistungsvorteil von einem zum anderen. Die erste Option ist etwas, das von FP inspiriert ist (Whitch wird allgemein als "netter" und "klarer" Weg bezeichnet, Ihren Code auszudrücken). In Wirklichkeit ist dies eher eine "Stil" -Frage.
Eugene Loy
5
@ Dwb: In diesem Fall ist das nicht relevant. forEach ist nicht als parallel oder ähnlich definiert, daher sind diese beiden Dinge semantisch äquivalent. Natürlich ist es möglich, eine parallele Version von forEach zu implementieren (und eine ist möglicherweise bereits in der Standardbibliothek vorhanden), und in einem solchen Fall wäre die Lambda-Ausdruckssyntax sehr nützlich.
AardvarkSoup
8
@AardvarkSoup Die Instanz, auf der forEach aufgerufen wird, ist ein Stream ( lambdadoc.net/api/java/util/stream/Stream.html ). Um eine parallele Ausführung anzufordern, könnte man joins.parallel (). ForEach (...)
mschenk74
13
Ist das joins.forEach((join) -> mIrc.join(mSession, join));wirklich eine "Vereinfachung" von for (String join : joins) { mIrc.join(mSession, join); }? Sie haben die Interpunktionszahl von 9 auf 12 erhöht, um den Typ von zu verbergen join. Was Sie wirklich getan haben, ist, zwei Aussagen in eine Zeile zu setzen.
Tom Hawtin - Tackline
7
Ein weiterer zu berücksichtigender Punkt ist die eingeschränkte Fähigkeit zur Erfassung von Variablen in Java. Mit Stream.forEach () können Sie lokale Variablen nicht aktualisieren, da sie durch ihre Erfassung endgültig sind. Dies bedeutet, dass Sie im forEach-Lambda ein statusbehaftetes Verhalten haben können (es sei denn, Sie sind auf eine gewisse Hässlichkeit wie die Verwendung von Klassenstatusvariablen vorbereitet).
RedGlyph

Antworten:

511

Die bessere Praxis ist zu verwenden for-each. Neben der Verletzung des Keep It Simple, Stupid- Prinzips weist der New-Fangled forEach()mindestens die folgenden Mängel auf:

  • Nicht endgültige Variablen können nicht verwendet werden . Code wie der folgende kann also nicht in ein forEach-Lambda umgewandelt werden:

    Object prev = null;
    for(Object curr : list)
    {
        if( prev != null )
            foo(prev, curr);
        prev = curr;
    }
  • Überprüfte Ausnahmen können nicht behandelt werden . Lambdas ist es eigentlich nicht verboten, geprüfte Ausnahmen auszulösen, aber gängige Funktionsschnittstellen wie Consumerdeklarieren keine. Daher muss jeder Code, der geprüfte Ausnahmen auslöst, diese in try-catchoder umschließen Throwables.propagate(). Aber selbst wenn Sie das tun, ist nicht immer klar, was mit der ausgelösten Ausnahme passiert. Es könnte irgendwo in den Eingeweiden von verschluckt werdenforEach()

  • Begrenzte Flusskontrolle . A returnin einem Lambda ist gleich a continuein a für jeden, aber es gibt kein Äquivalent zu a break. Es ist auch schwierig, Dinge wie Rückgabewerte, Kurzschluss oder gesetzte Flags zu tun (was die Dinge ein wenig gelindert hätte, wenn es nicht gegen die Regel der nicht endgültigen Variablen verstoßen hätte ). "Dies ist nicht nur eine Optimierung, sondern auch wichtig, wenn Sie bedenken, dass einige Sequenzen (wie das Lesen der Zeilen in einer Datei) Nebenwirkungen haben können oder Sie eine unendliche Sequenz haben können."

  • Könnte parallel ausgeführt werden , was für alle schrecklich ist, außer für 0,1% Ihres Codes, der optimiert werden muss. Jeder parallele Code muss durchdacht werden (auch wenn er keine Sperren, flüchtigen Stoffe und andere besonders unangenehme Aspekte der herkömmlichen Multithread-Ausführung verwendet). Jeder Fehler wird schwer zu finden sein.

  • Könnte die Leistung beeinträchtigen , da die JIT nicht für jedes () + Lambda im gleichen Maße wie einfache Loops optimieren kann, insbesondere jetzt, wo Lambdas neu sind. Mit "Optimierung" meine ich nicht den Aufwand für das Aufrufen von Lambdas (der klein ist), sondern die ausgefeilte Analyse und Transformation, die der moderne JIT-Compiler beim Ausführen von Code durchführt.

  • Wenn Sie Parallelität benötigen, ist es wahrscheinlich viel schneller und nicht viel schwieriger, einen ExecutorService zu verwenden . Streams sind sowohl automatisch (lesen Sie: Sie wissen nicht viel über Ihr Problem) als auch verwenden eine spezielle Parallelisierungsstrategie (lesen Sie: für den allgemeinen Fall ineffizient) ( Fork-Join-rekursive Zerlegung ).

  • Das Debuggen wird aufgrund der verschachtelten Aufrufhierarchie und, Gott bewahre, der parallelen Ausführung verwirrender . Der Debugger hat möglicherweise Probleme beim Anzeigen von Variablen aus dem umgebenden Code, und Dinge wie Step-Through funktionieren möglicherweise nicht wie erwartet.

  • Streams sind im Allgemeinen schwieriger zu codieren, zu lesen und zu debuggen . Tatsächlich gilt dies für komplexe " fließende " APIs im Allgemeinen. Die Kombination aus komplexen Einzelanweisungen, dem starken Einsatz von Generika und dem Fehlen von Zwischenvariablen führt zu verwirrenden Fehlermeldungen und einem frustrierten Debugging. Anstelle von "Diese Methode hat keine Überladung für Typ X" wird eine Fehlermeldung angezeigt, die näher an "Irgendwo, wo Sie die Typen durcheinander gebracht haben, aber wir wissen nicht, wo oder wie" liegt. Ebenso können Sie die Dinge in einem Debugger nicht so einfach durchgehen und untersuchen, wie wenn der Code in mehrere Anweisungen unterteilt ist und Zwischenwerte in Variablen gespeichert werden. Schließlich kann das Lesen des Codes und das Verstehen der Typen und des Verhaltens in jeder Ausführungsphase nicht trivial sein.

  • Sticht hervor wie ein schmerzender Daumen . Die Java-Sprache hat bereits die for-each-Anweisung. Warum durch einen Funktionsaufruf ersetzen? Warum sollte man ermutigen, Nebenwirkungen irgendwo in Ausdrücken zu verstecken? Warum unhandliche Einzeiler ermutigen? Regelmäßiges Mischen für jedes und neues für jedes wohl oder übel ist ein schlechter Stil. Code sollte in Redewendungen sprechen (Muster, die aufgrund ihrer Wiederholung schnell zu verstehen sind). Je weniger Redewendungen verwendet werden, desto klarer ist der Code und desto weniger Zeit wird für die Entscheidung verwendet, welche Redewendung verwendet werden soll (ein großer Zeitaufwand für Perfektionisten wie mich! ).

Wie Sie sehen, bin ich kein großer Fan von forEach (), außer in Fällen, in denen es Sinn macht.

Besonders anstößig für mich ist die Tatsache, dass Streames nicht implementiert wird Iterable(obwohl es tatsächlich eine Methode gibt iterator) und nicht in einem for-each verwendet werden kann, nur mit einem forEach (). Ich empfehle, Streams mit in Iterables zu verwandeln (Iterable<T>)stream::iterator. Eine bessere Alternative ist die Verwendung von StreamEx, mit dem eine Reihe von Stream-API-Problemen behoben werden, einschließlich der Implementierung Iterable.

Das heißt, forEach()ist nützlich für die folgenden:

  • Atomare Iteration über eine synchronisierte Liste . Zuvor war eine mit generierte Liste in Collections.synchronizedList()Bezug auf Dinge wie get oder set atomar, beim Iterieren jedoch nicht threadsicher.

  • Parallele Ausführung (unter Verwendung eines geeigneten parallelen Streams) . Dies erspart Ihnen ein paar Codezeilen im Vergleich zur Verwendung eines ExecutorService, wenn Ihr Problem mit den in Streams und Spliterators integrierten Leistungsannahmen übereinstimmt.

  • Bestimmte Container, die wie die synchronisierte Liste von der Kontrolle der Iteration profitieren (obwohl dies weitgehend theoretisch ist, es sei denn, die Benutzer können weitere Beispiele nennen).

  • Saubereres Aufrufen einer einzelnen Funktion mithilfe forEach()eines Methodenreferenzarguments (dh list.forEach (obj::someMethod)). Beachten Sie jedoch die Punkte zu aktivierten Ausnahmen, zum schwierigeren Debuggen und zur Reduzierung der Anzahl der Redewendungen, die Sie beim Schreiben von Code verwenden.

Artikel, die ich als Referenz verwendet habe:

BEARBEITEN: Es sieht so aus, als ob einige der ursprünglichen Vorschläge für Lambdas (wie http://www.javac.info/closures-v06a.html ) einige der von mir erwähnten Probleme gelöst haben (natürlich unter Hinzufügung eigener Komplikationen).

Aleksandr Dubinsky
quelle
95
"Warum ermutigen, Nebenwirkungen irgendwo in Ausdrücken zu verstecken?" ist die falsche Frage. Das Funktionale forEachsoll den funktionalen Stil fördern, dh Ausdrücke ohne Nebenwirkungen verwenden. Wenn Sie auf eine Situation stoßen, die forEachmit Ihren Nebenwirkungen nicht gut funktioniert, sollten Sie das Gefühl haben, dass Sie nicht das richtige Werkzeug für den Job verwenden. Dann lautet die einfache Antwort: Das liegt daran, dass Ihr Gefühl richtig ist. Bleiben Sie also bei jeder Schleife. Die klassische forSchleife wurde nicht veraltet ...
Holger
17
@Holger Wie kann forEachohne Nebenwirkungen verwendet werden?
Aleksandr Dubinsky
14
In Ordnung, ich war nicht präzise genug, forEachist die einzige Stream-Operation, die für Nebenwirkungen vorgesehen ist, aber nicht für Nebenwirkungen wie Ihren Beispielcode. Das Zählen ist eine typische reduceOperation. Ich würde in der Regel vorschlagen, jede Operation, die lokale Variablen manipuliert oder den Kontrollfluss (einschließlich Ausnahmebehandlung) beeinflusst, in einer klassischen forSchleife beizubehalten. In Bezug auf die ursprüngliche Frage, denke ich, ergibt sich das Problem aus der Tatsache, dass jemand einen Stream verwendet, bei dem eine einfache forSchleife über die Quelle des Streams ausreichen würde. Verwenden Sie einen Stream, in dem forEach()nur funktioniert
Holger
8
@Holger Was ist ein Beispiel für Nebenwirkungen, forEachfür die geeignet wäre?
Aleksandr Dubinsky
29
Etwas, das jedes Element einzeln verarbeitet und nicht versucht, lokale Variablen zu mutieren. ZB Manipulieren der Elemente selbst oder Drucken, Schreiben / Senden an eine Datei, einen Netzwerk-Stream usw. Es ist für mich kein Problem, wenn Sie diese Beispiele in Frage stellen und keine Anwendung dafür sehen. Filtern, Zuordnen, Reduzieren, Suchen und (in geringerem Maße) Sammeln sind die bevorzugten Operationen eines Streams. Das forEach scheint mir eine Annehmlichkeit für die Verknüpfung mit vorhandenen APIs zu sein. Und natürlich für den Parallelbetrieb. Diese funktionieren nicht mit forSchleifen.
Holger
169

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

  • Der Hauptvorteil aus meiner Sicht ist, dass die Implementierung dessen, was innerhalb der Schleife zu tun ist, 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önnen Sie einfach schreiben

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

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

Hinweis: Für meine Antwort habe ich angenommen, dass Joins die java.util.StreamSchnittstelle implementieren . Wenn Joins nur die java.util.IterableSchnittstelle implementiert, ist dies nicht mehr der Fall.

mschenk74
quelle
4
Die Folien eines Orakelingenieurs, auf den er sich bezieht ( blogs.oracle.com/darcy/resource/Devoxx/… ), erwähnen nicht die Parallelität innerhalb dieser Lambda-Ausdrücke. Die Parallelität kann innerhalb der Massensammelmethoden wie map& auftreten fold, die nicht wirklich mit Lambdas zusammenhängen.
Thomas Jungblut
1
Es scheint nicht wirklich, dass der OP-Code hier von der automatischen Parallelität profitieren wird (zumal es keine Garantie dafür gibt, dass es eine geben wird). Wir wissen nicht wirklich, was "mIrc" ist, aber "join" scheint nicht wirklich etwas zu sein, das außerhalb der Reihenfolge ausgeführt werden kann.
Eugene Loy
11
Stream#forEachund Iterable#forEachsind nicht dasselbe. OP fragt nach Iterable#forEach.
Gvlasov
2
Ich habe den UPDATEX-Stil verwendet, da sich die Spezifikation zwischen dem Zeitpunkt, zu dem die Frage gestellt wurde, und dem Zeitpunkt, zu dem die Antwort aktualisiert wurde, geändert hat. Ohne die Geschichte der Antwort wäre es noch verwirrender, dachte ich.
mschenk74
1
Könnte mir bitte jemand erklären, warum diese Antwort nicht gültig ist, wenn stattdessen joinsimplementiert Iterablewird Stream? joins.stream().forEach((join) -> mIrc.join(mSession, join));joins.parallelStream().forEach((join) -> mIrc.join(mSession, join));joinsIterable
Nach
112

Wenn man diese Frage liest, kann man den Eindruck gewinnen, dass Iterable#forEachin Kombination mit Lambda-Ausdrücken eine Abkürzung / ein Ersatz für das Schreiben einer traditionellen for-each-Schleife ist. Das ist einfach nicht wahr. Dieser Code aus dem OP:

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

ist nicht als Abkürzung zum Schreiben gedacht

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

und sollte auf keinen Fall auf diese Weise verwendet werden. Stattdessen ist es als Verknüpfung (obwohl es nicht genau dasselbe ist) zum Schreiben gedacht

joins.forEach(new Consumer<T>() {
    @Override
    public void accept(T join) {
        mIrc.join(mSession, join);
    }
});

Und es ist ein Ersatz für den folgenden Java 7-Code:

final Consumer<T> c = new Consumer<T>() {
    @Override
    public void accept(T join) {
        mIrc.join(mSession, join);
    }
};
for (T t : joins) {
    c.accept(t);
}

Das Ersetzen des Hauptteils einer Schleife durch eine Funktionsschnittstelle, wie in den obigen Beispielen, macht Ihren Code expliziter: Sie sagen, dass (1) der Hauptteil der Schleife den umgebenden Code und den Kontrollfluss nicht beeinflusst, und (2) der Der Hauptteil der Schleife kann durch eine andere Implementierung der Funktion ersetzt werden, ohne den umgebenden Code zu beeinflussen. Nicht auf nicht endgültige Variablen des äußeren Bereichs zugreifen zu können, ist kein Defizit an Funktionen / Lambdas, sondern ein Merkmal , das die Semantik Iterable#forEachvon der Semantik einer herkömmlichen for-each-Schleife unterscheidet. Sobald man sich an die Syntax von gewöhnt hat Iterable#forEach, wird der Code besser lesbar, da Sie sofort diese zusätzlichen Informationen über den Code erhalten.

Herkömmliche for-each-Schleifen bleiben in Java sicherlich eine gute Praxis (um den überstrapazierten Begriff " Best Practice " zu vermeiden ). Dies bedeutet jedoch nicht, dass Iterable#forEachdies als schlechte Praxis oder schlechter Stil angesehen werden sollte. Es ist immer eine gute Praxis, das richtige Werkzeug für die Arbeit zu verwenden, und dazu gehört auch das Mischen traditioneller for-each-Schleifen Iterable#forEach, wo dies sinnvoll ist.

Da die Nachteile von Iterable#forEachbereits in diesem Thread besprochen wurden, sind hier einige Gründe, warum Sie wahrscheinlich verwenden möchten Iterable#forEach:

  • So machen Sie Ihren Code expliziter: Wie oben beschrieben, Iterable#forEach kann Ihr Code in einigen Situationen expliziter und lesbarer werden.

  • So erweitern Sie Ihren Code erweiterbar und wartbar: Wenn Sie eine Funktion als Hauptteil einer Schleife verwenden, können Sie diese Funktion durch verschiedene Implementierungen ersetzen (siehe Strategiemuster ). Sie können beispielsweise den Lambda-Ausdruck leicht durch einen Methodenaufruf ersetzen, der von Unterklassen überschrieben werden kann:

    joins.forEach(getJoinStrategy());

    Anschließend können Sie Standardstrategien mithilfe einer Aufzählung bereitstellen, die die Funktionsschnittstelle implementiert. Dies macht Ihren Code nicht nur erweiterbarer, sondern erhöht auch die Wartbarkeit, da die Schleifenimplementierung von der Schleifendeklaration entkoppelt wird.

  • So machen Sie Ihren Code debuggbarer: Das Trennen der Schleifenimplementierung von der Deklaration kann auch das Debuggen vereinfachen, da Sie möglicherweise über eine spezielle Debug-Implementierung verfügen, die Debug-Meldungen druckt, ohne dass Sie Ihren Hauptcode überladen müssen if(DEBUG)System.out.println(). Die Debug-Implementierung kann beispielsweise ein Delegat sein , der die eigentliche Funktionsimplementierung dekoriert .

  • So optimieren Sie leistungskritischen Code: Entgegen einigen Aussagen in diesem Thread Iterable#forEach bietet er bereits eine bessere Leistung als eine herkömmliche Schleife für jede Schleife, zumindest wenn ArrayList verwendet und Hotspot im "-client" -Modus ausgeführt wird. Während diese Leistungssteigerung für die meisten Anwendungsfälle gering und vernachlässigbar ist, gibt es Situationen, in denen diese zusätzliche Leistung einen Unterschied machen kann. Zum Beispiel werden Bibliotheksverwalter sicherlich prüfen wollen, ob einige ihrer vorhandenen Schleifenimplementierungen durch ersetzt werden sollten Iterable#forEach.

    Um diese Aussage mit Fakten zu untermauern, habe ich mit Caliper einige Mikro-Benchmarks durchgeführt . Hier ist der Testcode (der neueste Caliper von git wird benötigt):

    @VmOptions("-server")
    public class Java8IterationBenchmarks {
    
        public static class TestObject {
            public int result;
        }
    
        public @Param({"100", "10000"}) int elementCount;
    
        ArrayList<TestObject> list;
        TestObject[] array;
    
        @BeforeExperiment
        public void setup(){
            list = new ArrayList<>(elementCount);
            for (int i = 0; i < elementCount; i++) {
                list.add(new TestObject());
            }
            array = list.toArray(new TestObject[list.size()]);
        }
    
        @Benchmark
        public void timeTraditionalForEach(int reps){
            for (int i = 0; i < reps; i++) {
                for (TestObject t : list) {
                    t.result++;
                }
            }
            return;
        }
    
        @Benchmark
        public void timeForEachAnonymousClass(int reps){
            for (int i = 0; i < reps; i++) {
                list.forEach(new Consumer<TestObject>() {
                    @Override
                    public void accept(TestObject t) {
                        t.result++;
                    }
                });
            }
            return;
        }
    
        @Benchmark
        public void timeForEachLambda(int reps){
            for (int i = 0; i < reps; i++) {
                list.forEach(t -> t.result++);
            }
            return;
        }
    
        @Benchmark
        public void timeForEachOverArray(int reps){
            for (int i = 0; i < reps; i++) {
                for (TestObject t : array) {
                    t.result++;
                }
            }
        }
    }

    Und hier sind die Ergebnisse:

    Bei Ausführung mit "-client" wird Iterable#forEachdie herkömmliche for-Schleife über eine ArrayList übertroffen, sie ist jedoch immer noch langsamer als die direkte Iteration über ein Array. Bei Ausführung mit "-server" ist die Leistung aller Ansätze ungefähr gleich.

  • Um optionale Unterstützung für die parallele Ausführung bereitzustellen: Es wurde hier bereits gesagt, dass die Möglichkeit, die funktionale Schnittstelle von Iterable#forEachparallel unter Verwendung von Streams auszuführen , sicherlich ein wichtiger Aspekt ist. Da Collection#parallelStream()nicht garantiert wird, dass die Schleife tatsächlich parallel ausgeführt wird, muss dies als optionales Merkmal betrachtet werden. Wenn Sie Ihre Liste mit durchlaufen list.parallelStream().forEach(...);, sagen Sie explizit: Diese Schleife unterstützt die parallele Ausführung, hängt jedoch nicht davon ab. Auch dies ist ein Merkmal und kein Defizit!

    Indem Sie die Entscheidung für die parallele Ausführung von Ihrer eigentlichen Schleifenimplementierung entfernen, können Sie Ihren Code optional optimieren, ohne den Code selbst zu beeinflussen. Dies ist eine gute Sache. Wenn die Standardimplementierung für parallele Streams nicht Ihren Anforderungen entspricht, hindert Sie niemand daran, Ihre eigene Implementierung bereitzustellen. Sie können z. B. eine optimierte Sammlung bereitstellen, die vom zugrunde liegenden Betriebssystem, der Größe der Sammlung, der Anzahl der Kerne und einigen Voreinstellungen abhängt.

    public abstract class MyOptimizedCollection<E> implements Collection<E>{
        private enum OperatingSystem{
            LINUX, WINDOWS, ANDROID
        }
        private OperatingSystem operatingSystem = OperatingSystem.WINDOWS;
        private int numberOfCores = Runtime.getRuntime().availableProcessors();
        private Collection<E> delegate;
    
        @Override
        public Stream<E> parallelStream() {
            if (!System.getProperty("parallelSupport").equals("true")) {
                return this.delegate.stream();
            }
            switch (operatingSystem) {
                case WINDOWS:
                    if (numberOfCores > 3 && delegate.size() > 10000) {
                        return this.delegate.parallelStream();
                    }else{
                        return this.delegate.stream();
                    }
                case LINUX:
                    return SomeVerySpecialStreamImplementation.stream(this.delegate.spliterator());
                case ANDROID:
                default:
                    return this.delegate.stream();
            }
        }
    }

    Das Schöne dabei ist, dass Ihre Loop-Implementierung diese Details nicht kennen oder sich nicht darum kümmern muss.

Balder
quelle
5
Sie haben eine interessante Sichtweise in dieser Diskussion und sprechen eine Reihe von Punkten an. Ich werde versuchen, sie anzusprechen. Sie schlagen vor, zwischen forEachund for-eachbasierend auf einigen Kriterien bezüglich der Art des Schleifenkörpers zu wechseln . Weisheit und Disziplin, solche Regeln zu befolgen, sind das Markenzeichen eines guten Programmierers. Solche Regeln sind auch sein Fluch, weil die Menschen um ihn herum ihnen entweder nicht folgen oder nicht zustimmen. ZB mit aktivierten oder nicht aktivierten Ausnahmen. Diese Situation scheint noch nuancierter zu sein. Aber wenn der Körper "keinen Einfluss auf den Surround-Code oder die Flusskontrolle hat", wird er dann nicht besser als Funktion herausgerechnet?
Aleksandr Dubinsky
4
Danke für die ausführlichen Kommentare Aleksandr. But, if the body "does not affect surround code or flow control," isn't factoring it out as a function better?. Ja, dies wird meiner Meinung nach häufig der Fall sein - das Ausklammern dieser Schleifen als Funktionen ist eine natürliche Folge.
Balder
2
In Bezug auf das Leistungsproblem - ich denke, es hängt sehr stark von der Art der Schleife ab. In einem Projekt, an dem ich arbeite, habe ich Iterable#forEachnur wegen der Leistungssteigerung Funktionsschleifen verwendet, die denen vor Java 8 ähneln . Das fragliche Projekt hat eine Hauptschleife ähnlich einer Spielschleife mit einer undefinierten Anzahl verschachtelter Unterschleifen, in die Clients Schleifenteilnehmer als Funktionen einstecken können. Eine solche Softwarestruktur profitiert stark davon Iteable#forEach.
Balder
7
Ganz am Ende meiner Kritik steht ein Satz: "Code sollte in Redewendungen sprechen, und je weniger Redewendungen verwendet werden, desto klarer ist der Code und desto weniger Zeit wird für die Entscheidung aufgewendet, welche Redewendung verwendet werden soll." Ich begann diesen Punkt zutiefst zu schätzen, als ich von C # zu Java wechselte.
Aleksandr Dubinsky
7
Das ist ein schlechtes Argument. Sie können es verwenden, um alles zu rechtfertigen, was Sie wollen: Warum sollten Sie keine for-Schleife verwenden, weil eine while-Schleife gut genug ist und das eine Redewendung weniger ist. Warum sollten Sie eine Schleife, einen Schalter oder eine try / catch-Anweisung verwenden, wenn goto all das und noch mehr kann?
Tapichu
13

forEach()kann so implementiert werden, dass sie schneller als für jede Schleife ist, da der Iterable den besten Weg kennt, seine Elemente zu iterieren, im Gegensatz zum Standard-Iterator. Der Unterschied ist also eine interne oder eine externe Schleife.

Zum Beispiel ArrayList.forEach(action)kann einfach als implementiert werden

for(int i=0; i<size; i++)
    action.accept(elements[i])

im Gegensatz zu der für jede Schleife, die viel Gerüst erfordert

Iterator iter = list.iterator();
while(iter.hasNext())
    Object next = iter.next();
    do something with `next`

Wir müssen jedoch auch zwei Gemeinkosten berücksichtigen, indem forEach()wir das Lambda-Objekt erstellen und das andere die Lambda-Methode aufrufen. Sie sind wahrscheinlich nicht signifikant.

Siehe auch http://journal.stuffwithstuff.com/2013/01/13/iteration-inside-and-out/ zum Vergleichen interner / externer Iterationen für verschiedene Anwendungsfälle.

ZhongYu
quelle
8
Warum kennt der Iterable den besten Weg, der Iterator jedoch nicht?
mschenk74
2
Kein wesentlicher Unterschied, aber zusätzlicher Code ist erforderlich, um sich an die Iteratorschnittstelle anzupassen, was möglicherweise teurer ist.
ZhongYu
1
@ zhong.j.yu Wenn Sie Collection implementieren, implementieren Sie auch Iterable. Es gibt also keinen Code-Overhead in Bezug auf "Hinzufügen von mehr Code zum Implementieren fehlender Schnittstellenmethoden", wenn dies Ihr Punkt ist. Wie mschenk74 sagte, scheint es keine Gründe zu geben, warum Sie Ihren Iterator nicht optimieren können, um zu wissen, wie Sie Ihre Sammlung bestmöglich durchlaufen können. Ich bin damit einverstanden, dass die Erstellung von Iteratoren möglicherweise Overhead verursacht, aber im Ernst, diese Dinge sind normalerweise so billig, dass man sagen kann, dass sie keine Kosten verursachen ...
Eugene Loy
4
Beispiel: Iteration eines Baums: void forEach(Consumer<T> v){leftTree.forEach(v);v.accept(rootElem);rightTree.forEach(v);}Dies ist eleganter als die externe Iteration, und Sie können entscheiden, wie Sie am besten synchronisieren
möchten
1
Komischerweise ist der einzige Kommentar in den String.joinMethoden (okay, falscher Join) "Anzahl der Elemente, die Arrays.stream-Overhead wahrscheinlich nicht wert sind." Also benutzen sie eine schicke for-Schleife.
Tom Hawtin - Tackline
7

TL; DR : List.stream().forEach()war der schnellste.

Ich hatte das Gefühl, ich sollte meine Ergebnisse aus der Benchmarking-Iteration hinzufügen. Ich habe einen sehr einfachen Ansatz gewählt (keine Benchmarking-Frameworks) und 5 verschiedene Methoden bewertet:

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

das Testverfahren und die Parameter

private List<Integer> 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; //so it won't get JITed out
}

Die Liste in dieser Klasse wird wiederholt und einige Mal doIt(Integer i)auf alle Mitglieder angewendet, jedes Mal über eine andere Methode. In der Hauptklasse führe ich die getestete Methode dreimal aus, um die JVM aufzuwärmen. Ich führe dann die Testmethode 1000 Mal aus und summiere die Zeit, die für jede Iterationsmethode benötigt wird (mitSystem.nanoTime() ). Nachdem das erledigt ist, dividiere 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 einer i5 4-Kern-CPU mit Java-Version 1.8.0_05 ausgeführt

klassisch for

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

Ausführungszeit: 4,21 ms

klassischer 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

Assaf
quelle
23
Wie bekommt man diese Zahlen? Welches Benchmark-Framework verwenden Sie? Wenn Sie keine verwenden und System.out.printlndiese Daten nur naiv anzeigen, sind alle Ergebnisse nutzlos.
Luiggi Mendoza
2
Kein Rahmen. Ich benutze System.nanoTime(). Wenn Sie die Antwort lesen, werden Sie sehen, wie es gemacht wurde. Ich denke nicht, dass es nutzlos macht, da dies eine relative Frage ist. Es ist mir egal, wie gut eine bestimmte Methode war, es ist mir egal, wie gut sie im Vergleich zu den anderen Methoden war.
Assaf
31
Und das ist der Zweck eines guten Mikro-Benchmarks. Da Sie solche Anforderungen nicht erfüllt haben, sind die Ergebnisse nutzlos.
Luiggi Mendoza
6
Ich kann empfehlen, stattdessen JMH kennenzulernen. Dies wird für Java selbst verwendet und ist sehr bemüht, die richtigen Zahlen zu erhalten: openjdk.java.net/projects/code-tools/jmh
dsvensson
1
Ich stimme @LuiggiMendoza zu. Es gibt keine Möglichkeit zu wissen, dass diese Ergebnisse konsistent oder gültig sind. Gott weiß, wie viele Benchmarks ich durchgeführt habe, die immer wieder unterschiedliche Ergebnisse melden, insbesondere abhängig von der Iterationsreihenfolge, der Größe und was nicht.
mmm
6

Ich habe das Gefühl, dass ich meinen Kommentar etwas erweitern muss ...

Über Paradigma \ Stil

Das ist wahrscheinlich der bemerkenswerteste Aspekt. FP wurde populär, weil man Nebenwirkungen vermeiden kann. Ich werde nicht näher darauf eingehen, welche Vor- und Nachteile 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 darauf zurückzuführen ist, dass mehr FP nach Java gebracht wird (ironischerweise würde ich sagen, dass forEach in reinem FP nicht viel Verwendung findet, da es nichts anderes als das Einführen tut Nebenwirkungen).

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

Über Parallelität.

Aus Sicht der Leistung gibt es keine versprochenen nennenswerten Vorteile bei der Verwendung von Iterable.forEach gegenüber foreach (...).

Laut offiziellen Dokumenten auf Iterable.forEach :

Führt die angegebene Aktion für den Inhalt der Iterable aus, in der Reihenfolge , in der Elemente beim Iterieren auftreten, bis alle Elemente verarbeitet wurden oder die Aktion eine Ausnahme auslöst .

... dh es ist ziemlich klar, dass es keine implizite Parallelität geben wird. Das Hinzufügen eines würde eine LSP-Verletzung darstellen.

Jetzt gibt es "Parallell-Sammlungen", die in Java 8 versprochen werden, aber um mit denen zu arbeiten, müssen Sie mich expliziter machen und besonders vorsichtig sein, um sie zu verwenden (siehe zum Beispiel die Antwort von mschenk74).

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

AKTUALISIEREN: Vielleicht nicht so offensichtlich und auf einen Blick etwas gedehnt, aber es gibt noch eine andere Facette von Stil und Lesbarkeit.

Zuallererst - einfache alte Forloops sind einfach und alt. Jeder kennt sie bereits.

Zweitens und noch wichtiger: Sie möchten Iterable.forEach wahrscheinlich nur mit einzeiligen Lambdas verwenden. Wenn "Körper" schwerer wird, sind sie in der Regel nicht so lesbar. Von hier aus haben Sie zwei Möglichkeiten: Verwenden Sie innere Klassen (yuck) oder verwenden Sie einen einfachen alten Forloop. Menschen ärgern sich oft, wenn sie sehen, dass dieselben Dinge (Iterationen über Sammlungen) auf verschiedene Arten / Stile in derselben Codebasis ausgeführt werden, und dies scheint der Fall zu sein.

Auch dies könnte ein Problem sein oder auch nicht. Kommt auf Leute an, die an Code arbeiten.

Eugene Loy
quelle
1
Parallelität benötigt keine neuen "parallelen Sammlungen". Es hängt nur davon ab, ob Sie nach einem sequentiellen Stream (mit collection.stream ()) oder nach einem parallelen (mit collection.parallelStream ()) gefragt haben.
JB Nizet
@JBNizet Laut docs sammelt Collection.parallelStream () nicht, dass die Implementierung der Sammlung einen Parallell-Stream zurückgibt. Ich frage mich eigentlich, wann dies passieren könnte, aber wahrscheinlich hängt dies von der Sammlung ab.
Eugene Loy
einverstanden. Es kommt auch auf die Sammlung an. Mein Punkt war jedoch, dass parallele foreach-Schleifen bereits für alle Standardsammlungen (ArrayList usw.) verfügbar waren. Sie müssen nicht auf "parallele Sammlungen" warten.
JB Nizet
@JBNizet sind sich in Ihrem Punkt einig, aber das habe ich eigentlich nicht mit "parallelen Sammlungen" gemeint. Ich verweise auf Collection.parallelStream (), das in Java 8 als "parallele Sammlungen" hinzugefügt wurde, in Analogie zu Scalas Konzept, das fast dasselbe tut. Ich bin mir auch nicht sicher, wie es in JSRs Bit heißt. Ich habe einige Artikel gesehen, die dieselbe Terminologie für diese Java 8-Funktion verwenden.
Eugene Loy
1
Für den letzten Absatz können Sie eine Funktionsreferenz verwenden:collection.forEach(MyClass::loopBody);
Ratschenfreak
6

Eine der aufregendsten Funktionen forEach ist das Fehlen einer Unterstützung für geprüfte Ausnahmen.

Eine mögliche Problemumgehung besteht darin, das Terminal forEachdurch eine einfache alte foreach-Schleife zu ersetzen :

    Stream<String> stream = Stream.of("", "1", "2", "3").filter(s -> !s.isEmpty());
    Iterable<String> iterable = stream::iterator;
    for (String s : iterable) {
        fileWriter.append(s);
    }

Hier ist eine Liste der beliebtesten Fragen mit anderen Problemumgehungen zur Behandlung geprüfter Ausnahmen in Lambdas und Streams:

Java 8 Lambda-Funktion, die eine Ausnahme auslöst?

Java 8: Lambda-Streams, Filter nach Methode mit Ausnahme

Wie kann ich CHECKED-Ausnahmen aus Java 8-Streams heraus auslösen?

Java 8: Obligatorisch geprüfte Ausnahmebehandlung in Lambda-Ausdrücken. Warum obligatorisch, nicht optional?

Vadzim
quelle
3

Der Vorteil der Java 1.8 forEach-Methode gegenüber 1.7 Enhanced for loop besteht darin, dass Sie sich beim Schreiben von Code nur auf die Geschäftslogik konzentrieren können.

Die forEach-Methode verwendet also das Objekt java.util.function.Consumer als Argument Daher ist es hilfreich, unsere Geschäftslogik an einem separaten Speicherort zu haben, damit Sie sie jederzeit wiederverwenden können.

Schauen Sie sich das folgende Snippet an.

  • Hier habe ich eine neue Klasse erstellt, die die Methode "Akzeptanzklasse" aus der Verbraucherklasse überschreibt, in der Sie zusätzliche Funktionen hinzufügen können, "Mehr als Iteration". !!!!!!

    class MyConsumer implements Consumer<Integer>{
    
        @Override
        public void accept(Integer o) {
            System.out.println("Here you can also add your business logic that will work with Iteration and you can reuse it."+o);
        }
    }
    
    public class ForEachConsumer {
    
        public static void main(String[] args) {
    
            // Creating simple ArrayList.
            ArrayList<Integer> aList = new ArrayList<>();
            for(int i=1;i<=10;i++) aList.add(i);
    
            //Calling forEach with customized Iterator.
            MyConsumer consumer = new MyConsumer();
            aList.forEach(consumer);
    
    
            // Using Lambda Expression for Consumer. (Functional Interface) 
            Consumer<Integer> lambda = (Integer o) ->{
                System.out.println("Using Lambda Expression to iterate and do something else(BI).. "+o);
            };
            aList.forEach(lambda);
    
            // Using Anonymous Inner Class.
            aList.forEach(new Consumer<Integer>(){
                @Override
                public void accept(Integer o) {
                    System.out.println("Calling with Anonymous Inner Class "+o);
                }
            });
        }
    }
Hardik Patel
quelle