Frage In Java Streams ist wirklich nur zum Debuggen spähen?


Ich lese über Java-Streams und entdecke neue Dinge, während ich mitfahre. Eines der neuen Dinge, die ich fand, war die peek() Funktion. Fast alles, was ich gelesen habe, sagt, es sollte verwendet werden, um Ihre Streams zu debuggen.

Was wäre, wenn ich einen Stream hätte, bei dem jedes Konto einen Benutzernamen, ein Passwortfeld und eine login () - und loggedIn () -Methode hat?

ich habe auch

Consumer<Account> login = account -> account.login();

und

Predicate<Account> loggedIn = account -> account.loggedIn();

Warum sollte das so schlimm sein?

List<Account> accounts; //assume it's been setup
List<Account> loggedInAccount = 
accounts.stream()
    .peek(login)
    .filter(loggedIn)
    .collect(Collectors.toList());

Soweit ich das beurteilen kann, macht das genau das, was es beabsichtigt. Es;

  • Nimmt eine Liste von Konten
  • Versucht, sich bei jedem Konto anzumelden
  • Filtert alle Konten aus, die nicht angemeldet sind
  • Sammelt die eingeloggten Konten in einer neuen Liste

Was ist der Nachteil, wenn man so etwas macht? Warum sollte ich nicht weitermachen? Zuletzt, wenn nicht diese Lösung dann was?

Die ursprüngliche Version von diesem verwendet die .filter () -Methode wie folgt;

.filter(account -> {
        account.login();
        return account.loggedIn();
    })

76
2017-11-10 17:12


Ursprung


Antworten:


Der Schlüssel zum Erfolg:

Verwenden Sie die API nicht in einer unbeabsichtigten Weise, auch wenn sie Ihr unmittelbares Ziel erreicht. Dieser Ansatz kann in der Zukunft brechen, und es ist auch für zukünftige Betreuer unklar.


Es kann nicht schaden, dies auf mehrere Operationen zu verteilen, da es sich um verschiedene Operationen handelt. Dort ist schaden bei der Verwendung der API auf eine unklare und unbeabsichtigte Weise, die Auswirkungen haben kann, wenn dieses bestimmte Verhalten in zukünftigen Versionen von Java geändert wird.

Verwenden forEach Auf diese Operation würde dem Betreuer klar machen, dass es eine beabsichtigt Nebeneffekt auf jedes Element von accountsund dass Sie eine Operation ausführen, die sie mutieren kann.

Es ist auch konventioneller in dem Sinne, dass peek ist eine Zwischenoperation, die nicht an der gesamten Sammlung arbeitet, bis der Terminalbetrieb läuft, sondern forEach ist in der Tat eine Terminaloperation. Auf diese Weise können Sie starke Argumente für das Verhalten und den Fluss Ihres Codes festlegen, anstatt Fragen zu if zu stellen peek würde sich genauso verhalten wie forEach tut in diesem Zusammenhang.

accounts.forEach(a -> a.login());
List<Account> loggedInAccounts = accounts.stream()
                                         .filter(Account::loggedIn)
                                         .collect(Collectors.toList());

49
2017-11-10 17:55



Das Wichtigste, was Sie verstehen müssen, ist, dass die Ströme von der Terminalbetrieb. Die Terminaloperation bestimmt, ob alle Elemente verarbeitet werden müssen oder überhaupt. Damit collect ist eine Operation, die jedes Element verarbeitet, während findAny kann die Verarbeitung von Elementen stoppen, sobald ein übereinstimmendes Element gefunden wurde.

Und count() verarbeitet möglicherweise überhaupt keine Elemente, wenn es die Größe des Streams ermitteln kann, ohne die Elemente zu verarbeiten. Da dies eine Optimierung ist, die nicht in Java 8 gemacht wird, aber in Java 9 enthalten ist, kann es zu Überraschungen kommen, wenn Sie zu Java 9 wechseln und sich auf Code verlassen count() Verarbeitung aller Artikel Dies ist auch mit anderen von der Implementierung abhängigen Details verbunden, z. Selbst in Java 9 wird die Referenzimplementierung nicht in der Lage sein, die Größe einer unendlichen Stream-Quelle, kombiniert mit limit während es keine grundsätzliche Beschränkung gibt, die eine solche Vorhersage verhindert.

Schon seit peek ermöglicht die Ausführung der bereitgestellten Aktion für jedes Element wie Elemente aus dem resultierenden Strom verbraucht werden", Es erfordert keine Verarbeitung von Elementen, sondern führt die Aktion abhängig davon aus, was die Terminaloperation benötigt. Dies bedeutet, dass Sie es mit großer Sorgfalt verwenden müssen, wenn Sie eine bestimmte Verarbeitung benötigen, z. möchte eine Aktion auf alle Elemente anwenden. Es funktioniert, wenn die Terminaloperation garantiert alle Elemente verarbeitet, aber selbst dann müssen Sie sicher sein, dass nicht der nächste Entwickler die Terminaloperation ändert (oder Sie diesen subtilen Aspekt vergessen).

Während Streams gewährleisten, dass die Begegnungs- Reihenfolge für bestimmte Kombinationen von Operationen auch für parallele Streams beibehalten wird, gelten diese Garantien nicht für peek. Beim Sammeln in einer Liste hat die resultierende Liste die richtige Reihenfolge für geordnete parallele Ströme, aber die peek Aktion kann in einer beliebigen Reihenfolge und gleichzeitig aufgerufen werden.

Also die nützlichste Sache, die Sie tun können peek ist herauszufinden, ob ein Stream-Element verarbeitet wurde, was genau in der API-Dokumentation steht:

Diese Methode gibt es hauptsächlich, um das Debugging zu unterstützen, wo Sie die Elemente sehen möchten, die über einen bestimmten Punkt in einer Pipeline fließen


68
2017-11-10 17:50



Vielleicht sollte eine Faustregel lauten, dass Sie, wenn Sie einen Blick außerhalb des "Debug" -Szenarios verwenden, dies nur tun sollten, wenn Sie sicher sind, was die Filterbedingungen für das Beenden und die Zwischenfilterung sind. Beispielsweise:

return list.stream().map(foo->foo.getBar())
                    .peek(bar->bar.publish("HELLO"))
                    .collect(Collectors.toList());

scheint ein gültiger Fall zu sein, wo Sie wollen, in einem Vorgang alle Foos in Bars zu verwandeln und ihnen allen Hallo zu sagen.

Scheint effizienter und eleganter als etwas wie:

List<Bar> bars = list.stream().map(foo->foo.getBar()).collect(Collectors.toList());
bars.forEach(bar->bar.publish("HELLO"));
return bars;

und Sie enden nicht damit, eine Sammlung zweimal zu wiederholen.


9
2017-11-11 12:52



Obwohl ich den meisten Antworten zustimme, habe ich einen Fall, in dem die Verwendung von Peek tatsächlich der sauberste Weg zu sein scheint.

Entscheiden Sie sich, ähnlich wie bei Ihrem Anwendungsfall, nur für aktive Konten und führen Sie dann eine Anmeldung für diese Konten durch.

accounts.stream()
    .filter(Account::isActive)
    .peek(login)
    .collect(Collectors.toList());

Peek ist hilfreich, um den redundanten Aufruf zu vermeiden, ohne die Sammlung zweimal durchlaufen zu müssen:

accounts.stream()
    .filter(Account::isActive)
    .map(account -> {
        account.login();
        return account;
    })
    .collect(Collectors.toList());

2
2017-10-26 14:45



Ich würde sagen, dass peek bietet die Möglichkeit zu Dezentralisieren Sie Code, der Stream-Objekte mutieren oder den globalen Status ändern kann (basierend auf ihnen), anstatt alles in eine einfache oder zusammengesetzte Funktion an eine Terminal-Methode übergeben.

Nun könnte die Frage lauten: sollten wir Stream-Objekte mutieren oder den globalen Status innerhalb von Funktionen in der Java-Programmierung mit funktionalem Stil verändern??

Wenn die Antwort auf eine der obigen 2 Fragen ja (oder in einigen Fällen ja) ist, dann peek() ist definitiv nicht nur für Debugging-Zwecke, aus dem gleichen Grund, dass forEach() ist nicht nur für Debugging-Zwecke.

Für mich bei der Wahl zwischen forEach() und peek()Wählen Sie Folgendes aus: Soll ich Codeelemente, die Streamobjekte mutieren, an ein Composable anhängen, oder möchte ich, dass sie direkt an den Stream angehängt werden?

Ich denke peek() wird besser mit java9 Methoden gepaart. z.B. takeWhile() Möglicherweise müssen Sie entscheiden, wann die Iteration basierend auf einem bereits mutierten Objekt gestoppt werden soll forEach() hätte nicht den gleichen Effekt.

P.S. Ich habe nicht darauf verwiesen map() überall, denn wenn wir Objekte (oder einen globalen Zustand) mutieren wollen, anstatt neue Objekte zu erzeugen, funktioniert es genauso peek().


1
2018-06-21 14:14