Ich lese über Java-Streams und entdecke dabei neue Dinge. Eines der neuen Dinge, die ich gefunden habe, war die peek()
Funktion. Fast alles, was ich auf Peek gelesen habe, besagt, dass es zum Debuggen Ihrer Streams verwendet werden sollte.
Was wäre, wenn ich einen Stream hätte, in dem jedes Konto einen Benutzernamen, ein Kennwortfeld sowie eine Methode zum Anmelden () und Anmelden () 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 dies genau das, was es tun soll. Es;
- Nimmt eine Liste von Konten
- Versucht, sich bei jedem Konto anzumelden
- Filtert alle Konten heraus, die nicht angemeldet sind
- Sammelt die angemeldeten Konten in einer neuen Liste
Was ist der Nachteil von so etwas? Gibt es einen Grund, warum ich nicht fortfahren sollte? Wenn nicht diese Lösung, was dann?
In der Originalversion wurde die .filter () -Methode wie folgt verwendet:
.filter(account -> {
account.login();
return account.loggedIn();
})
java
java-8
java-stream
peek
Adam.J
quelle
quelle
forEach
möglicherweise die gewünschte Operation istpeek
. Nur weil es in der API enthalten ist, heißt das nicht, dass es nicht für Missbrauch offen ist (wieOptional.of
)..peek(Account::login)
und sein kann.filter(Account::loggedIn)
; Es gibt keinen Grund, ein Consumer and Predicate zu schreiben, das nur eine andere Methode wie diese aufruft.forEach()
undpeek()
kann nur über Nebenwirkungen ausgeführt werden. Diese sollten mit Vorsicht verwendet werden. ”. Meine Bemerkung war eher, um daran zu erinnern, dass diepeek
Operation (die für Debugging-Zwecke entwickelt wurde) nicht durch dasselbe in einer anderen Operation wiemap()
oder ersetzt werden solltefilter()
.Antworten:
Der Schlüssel zum Erfolg:
Verwenden Sie die API nicht unbeabsichtigt, auch wenn sie Ihr unmittelbares Ziel erreicht. Dieser Ansatz könnte in Zukunft brechen, und es ist auch für zukünftige Betreuer unklar.
Es schadet nicht, dies auf mehrere Operationen aufzuteilen, da es sich um unterschiedliche Operationen handelt. Es ist schädlich, die API auf unklare und unbeabsichtigte Weise zu verwenden. Dies kann Auswirkungen haben, wenn dieses bestimmte Verhalten in zukünftigen Versionen von Java geändert wird.
Die Verwendung
forEach
dieser Operation würde dem Betreuer klar machen, dass für jedes Element von eine beabsichtigte Nebenwirkung vorliegtaccounts
und dass Sie eine Operation ausführen, die es mutieren kann.Es ist auch konventioneller in dem Sinne, dass
peek
es sich um eine Zwischenoperation handelt, die nicht die gesamte Sammlung bearbeitet, bis die Terminaloperation ausgeführt wird, sondernforEach
tatsächlich eine Terminaloperation ist. Auf diese Weise können Sie starke Argumente für das Verhalten und den Fluss Ihres Codes vorbringen, anstatt Fragen zu stellen, ob Siepeek
sich genauso verhalten würden wieforEach
in diesem Kontext.quelle
forEach
direkt an der Quellensammlung durchführen:accounts.forEach(a -> a.login());
login()
Methode einenboolean
Wert zurückgab , der den Erfolgsstatus angibt…login()
a zurückgegeben wirdboolean
, können Sie es als Prädikat verwenden, das die sauberste Lösung darstellt. Es hat immer noch einen Nebeneffekt, aber das ist inlogin
Ordnung, solange es nicht stört, dh der Prozess einesAccount
hat keinen Einfluss auf den Anmeldeprozess eines anderenAccount
.Das Wichtige, was Sie verstehen müssen, ist, dass Streams vom Terminalbetrieb gesteuert werden . Die Terminaloperation bestimmt, ob alle Elemente verarbeitet werden müssen oder überhaupt. Dies
collect
ist eine Operation, die jedes Element verarbeitet, währendfindAny
die Verarbeitung von Elementen möglicherweise beendet wird, sobald ein übereinstimmendes Element gefunden wird.Und
count()
möglicherweise werden überhaupt keine Elemente verarbeitet, wenn die Größe des Streams bestimmt werden kann, ohne die Elemente zu verarbeiten. Da dies eine Optimierung ist, die nicht in Java 8, sondern in Java 9 vorgenommen wurde, kann es zu Überraschungen kommen, wenn Sie zu Java 9 wechseln und Code haben, der auf dercount()
Verarbeitung aller Elemente beruht . Dies hängt auch mit anderen implementierungsabhängigen Details zusammen, z. B. kann die Referenzimplementierung selbst in Java 9 die Größe einer unendlichen Stream-Quelle in Kombination nicht vorhersagen,limit
während es keine grundlegende Einschränkung gibt, die eine solche Vorhersage verhindert.Da
peek
die Ausführung der bereitgestellten Aktion für jedes Element möglich ist, wenn Elemente aus dem resultierenden Stream verbraucht werden , ist keine Verarbeitung von Elementen erforderlich, sondern die Aktion wird abhängig von den Anforderungen der Terminaloperation ausgeführt. Dies bedeutet, dass Sie es mit großer Sorgfalt verwenden müssen, wenn Sie eine bestimmte Verarbeitung benötigen, z. B. eine Aktion auf alle Elemente anwenden möchten. 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 garantieren, dass die Begegnungsreihenfolge für eine bestimmte Kombination 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 Streams, diepeek
Aktion kann jedoch in beliebiger Reihenfolge und gleichzeitig aufgerufen werden.Das Nützlichste, was Sie tun können,
peek
ist herauszufinden, ob ein Stream-Element verarbeitet wurde. Genau das steht in der API-Dokumentation:quelle
Vielleicht sollte eine Faustregel lauten: Wenn Sie Peek außerhalb des "Debug" -Szenarios verwenden, sollten Sie dies nur tun, wenn Sie sich über die Bedingungen für die Beendigung und Zwischenfilterung sicher sind. Beispielsweise:
scheint ein gültiger Fall zu sein, in dem Sie in einem Arbeitsgang alle Foos in Bars umwandeln und ihnen alle Hallo sagen möchten.
Scheint effizienter und eleganter als so etwas wie:
und am Ende iterieren Sie eine Sammlung nicht zweimal.
quelle
Ich würde sagen, dass dies
peek
die Möglichkeit bietet, Code zu dezentralisieren, der Stream-Objekte mutieren oder den globalen Status (basierend auf ihnen) ändern kann, anstatt alles in eine einfache oder zusammengesetzte Funktion zu packen, die an eine Terminalmethode übergeben wird.Nun könnte die Frage lauten: Sollten wir Stream-Objekte mutieren oder den globalen Status innerhalb von Funktionen in der Java-Programmierung im funktionalen Stil ändern ?
Wenn die Antwort auf eine der beiden oben genannten Fragen Ja lautet (oder: in einigen Fällen Ja), dann
peek()
ist dies definitiv nicht nur für Debugging-Zwecke , aus demselben Grund, derforEach()
nicht nur für Debugging-Zwecke gilt .Wenn ich zwischen
forEach()
und wählepeek()
, wähle ich Folgendes: Soll Code-Teile, die Stream-Objekte mutieren, an ein Composable angehängt werden, oder sollen sie direkt an Stream angehängt werden?Ich denke,
peek()
wird besser mit Java9-Methoden koppeln. z. B. musstakeWhile()
möglicherweise entschieden werden, wann die Iteration basierend auf einem bereits mutierten Objekt gestoppt werden soll, sodass das Parieren mitforEach()
nicht den gleichen Effekt hätte.PS Ich habe
map()
nirgendwo darauf verwiesen , weil es genau so funktioniert, wenn wir Objekte (oder den globalen Status) mutieren möchten, anstatt neue Objekte zu generierenpeek()
.quelle
Obwohl ich den meisten Antworten oben zustimme, habe ich einen Fall, in dem die Verwendung von Peek tatsächlich der sauberste Weg zu sein scheint.
Angenommen, Sie möchten ähnlich wie in Ihrem Anwendungsfall nur nach aktiven Konten filtern und sich dann für diese Konten anmelden.
Peek ist hilfreich, um den redundanten Aufruf zu vermeiden, ohne die Sammlung zweimal durchlaufen zu müssen:
quelle
Die funktionale Lösung besteht darin, das Kontoobjekt unveränderlich zu machen. Daher muss account.login () ein neues Kontoobjekt zurückgeben. Dies bedeutet, dass die Kartenoperation für die Anmeldung anstelle von Peek verwendet werden kann.
quelle