In Java stream è davvero solo il debugging?


137

Sto leggendo sui flussi Java e scoprendo nuove cose mentre procedo. Una delle novità che ho scoperto è stata la peek()funzione. Quasi tutto quello che ho letto su Peek dice che dovrebbe essere usato per eseguire il debug dei tuoi stream.

E se avessi un flusso in cui ogni account ha un nome utente, un campo password e un metodo login () e loggIn ().

Ho anche

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

e

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

Perché questo sarebbe così male?

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

Ora, per quanto posso dire, fa esattamente quello che è destinato a fare. It;

  • Prende un elenco di account
  • Cerca di accedere a ciascun account
  • Filtra tutti gli account che non hanno effettuato l'accesso
  • Raccoglie gli account registrati in un nuovo elenco

Qual è il lato negativo di fare qualcosa del genere? Qualche motivo per cui non dovrei procedere? Infine, se non questa soluzione, allora?

La versione originale di questo utilizzava il metodo .filter () come segue;

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

38
Ogni volta che mi ritrovo a necessitare di un lambda multilinea, sposto le linee in un metodo privato e passo il riferimento al metodo anziché al lambda.
VGR,

1
Qual è l'intento: stai cercando di accedere a tutti gli account e filtrarli in base al loro accesso (che può essere banalmente vero)? Oppure, vuoi effettuare il login, quindi filtrarli in base al fatto che abbiano effettuato l'accesso? Lo sto chiedendo in questo ordine perché forEachpotrebbe essere l'operazione desiderata al contrario peek. Solo perché è nell'API non significa che non sia aperto per abuso (come Optional.of).
Makoto,

8
Nota anche che il tuo codice potrebbe essere .peek(Account::login)e .filter(Account::loggedIn); non c'è motivo di scrivere un consumatore e un predicato che chiama semplicemente un altro metodo del genere.
Joshua Taylor,

2
Si noti inoltre che l'API del flusso scoraggia esplicitamente gli effetti collaterali nei parametri comportamentali .
Didier L

6
I consumatori utili hanno sempre effetti collaterali, questi ovviamente non sono scoraggiati. Questo è effettivamente menzionato nella stessa sezione: “ Un piccolo numero di operazioni di streaming, come forEach()e peek(), può operare solo tramite effetti collaterali; questi dovrebbero essere usati con cura. ”. La mia osservazione è stata più di ricordare che l' peekoperazione (che è progettata per scopi di debug) non dovrebbe essere sostituita facendo la stessa cosa all'interno di un'altra operazione come map()o filter().
Didier L

Risposte:


77

La chiave da asporto da questo:

Non utilizzare l'API in modo non intenzionale, anche se raggiunge il tuo obiettivo immediato. Tale approccio potrebbe rompersi in futuro ed è anche poco chiaro per i futuri manutentori.


Non vi è alcun danno nel rompere questo a più operazioni, in quanto si tratta di operazioni distinte. L' uso dell'API è dannoso in modo non chiaro e non intenzionale, il che può avere conseguenze se questo comportamento particolare viene modificato nelle future versioni di Java.

L'uso di forEachquesta operazione chiarirebbe al manutentore che esiste un effetto collaterale previsto su ciascun elemento di accountse che si sta eseguendo un'operazione che può mutarlo.

È anche più convenzionale nel senso che peekè un'operazione intermedia che non opera sull'intera raccolta fino a quando l'operazione terminale non viene eseguita, ma forEachè effettivamente un'operazione terminale. In questo modo, puoi fare forti argomentazioni sul comportamento e sul flusso del tuo codice invece di porre domande sul fatto che peeksi comporterebbe come forEachin questo contesto.

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

3
Se si esegue l'accesso in una fase di preelaborazione, non è necessario alcun flusso. Puoi esibirti forEachdirettamente nella raccolta dei sorgenti:accounts.forEach(a -> a.login());
Holger,

1
@Holger: punto eccellente. L'ho incorporato nella risposta.
Makoto,

2
@ Adam.J: Giusto, la mia risposta si è concentrata maggiormente sulla domanda generale contenuta nel tuo titolo, cioè questo metodo è davvero solo per il debug, spiegando gli aspetti di quel metodo. Questa risposta è più fusa sul tuo caso d'uso reale e su come farlo invece. Quindi si potrebbe dire che insieme forniscono il quadro completo. In primo luogo, il motivo per cui questo non è l'uso previsto, in secondo luogo la conclusione, non attenersi a un uso non intenzionale e cosa fare invece. Quest'ultimo avrà un uso più pratico per te.
Holger,

2
Certo, sarebbe stato molto più semplice se il login()metodo avesse restituito un booleanvalore che indicava lo stato di successo ...
Holger,

3
Questo è quello a cui miravo. Se login()restituisce a boolean, puoi usarlo come predicato che è la soluzione più pulita. Ha ancora un effetto collaterale, ma va bene fintanto che non interferisce, cioè il loginprocesso di uno Accountnon ha alcuna influenza sul processo di login di un altro Account.
Holger,

111

La cosa importante che devi capire è che i flussi sono guidati dal funzionamento del terminale . L'operazione del terminale determina se tutti gli elementi devono essere elaborati o tutti. Lo stesso collectvale per un'operazione che elabora ciascun elemento, mentre findAnypuò interrompere l'elaborazione degli articoli una volta rilevato un elemento corrispondente.

E count()potrebbe non elaborare alcun elemento quando può determinare la dimensione del flusso senza elaborare gli elementi. Poiché si tratta di un'ottimizzazione non realizzata in Java 8, ma che sarà in Java 9, potrebbero verificarsi sorprese quando si passa a Java 9 e si ha il codice che si basa count()sull'elaborazione di tutti gli elementi. Questo è anche collegato ad altri dettagli dipendenti dall'implementazione, ad esempio anche in Java 9, l'implementazione di riferimento non sarà in grado di prevedere la dimensione di una sorgente di flusso infinita combinata con limitmentre non vi è alcuna limitazione fondamentale che impedisce tale previsione.

Poiché peekconsente di "eseguire l'azione fornita su ciascun elemento quando gli elementi vengono consumati dal flusso risultante ", non impone l'elaborazione degli elementi ma eseguirà l'azione in base alle esigenze dell'operazione del terminale. Ciò implica che devi usarlo con grande cura se hai bisogno di una particolare elaborazione, ad esempio vuoi applicare un'azione su tutti gli elementi. Funziona se l'operazione del terminale è garantita per elaborare tutti gli elementi, ma anche in questo caso, devi essere sicuro che non lo sviluppatore successivo cambi l'operazione del terminale (o dimentichi quell'aspetto sottile).

Inoltre, mentre i flussi garantiscono di mantenere l'ordine di incontro per una determinata combinazione di operazioni anche per i flussi paralleli, queste garanzie non si applicano a peek. Quando si raccoglie in un elenco, l'elenco risultante avrà il giusto ordine per i flussi paralleli ordinati, ma l' peekazione può essere invocata in un ordine arbitrario e contemporaneamente.

Quindi la cosa più utile che puoi fare peekè scoprire se è stato elaborato un elemento stream che è esattamente ciò che dice la documentazione API:

Questo metodo esiste principalmente per supportare il debug, in cui si desidera vedere gli elementi mentre passano oltre un certo punto in una pipeline


ci saranno problemi, futuri o presenti, nel caso d'uso di OP? Il suo codice fa sempre quello che vuole?
ZhongYu,

9
@ bayou.io: per quanto posso vedere, non c'è nessun problema in questa forma esatta . Ma come ho cercato di spiegare, usarlo in questo modo implica che devi ricordare questo aspetto, anche se ritorni al codice uno o due anni dopo per incorporare «richiesta di funzionalità 9876» nel codice ...
Holger,

1
"l'azione sbirciata può essere invocata in un ordine arbitrario e contemporaneamente". Questa affermazione non va contro la loro regola per come funziona la sbirciatina, ad esempio "man mano che gli elementi vengono consumati"?
Jose Martinez,

5
@Jose Martinez: dice "poiché gli elementi vengono consumati dal flusso risultante ", che non è l'azione terminale ma l'elaborazione, anche se anche l'azione terminale potrebbe consumare elementi fuori ordine purché il risultato finale sia coerente. Ma penso anche, la frase della nota API, " vedere gli elementi mentre passano oltre un certo punto in una pipeline " fa un lavoro migliore nel descriverlo.
Holger,

23

Forse una regola empirica dovrebbe essere che se usi la sbirciatina al di fuori dello scenario "debug", dovresti farlo solo se sei sicuro di quali siano le condizioni di filtro intermedio e di terminazione. Per esempio:

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

sembra essere un caso valido dove vuoi, in una sola operazione per trasformare tutti i Foos in Bar e salutarli tutti.

Sembra più efficiente ed elegante di qualcosa di simile:

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

e non finisci per ripetere due volte una raccolta.


4

Direi che peekoffre la possibilità di decentralizzare il codice che può mutare gli oggetti di flusso o modificare lo stato globale (basato su di essi), invece di inserire tutto in una funzione semplice o composta passata a un metodo terminale.

Ora la domanda potrebbe essere: dovremmo mutare gli oggetti stream o cambiare lo stato globale all'interno delle funzioni nella programmazione Java in stile funzionale ?

Se la risposta a una delle 2 domande precedenti è sì (o: in alcuni casi sì), allora non peek()è sicuramente solo a scopo di debug , per lo stesso motivo che forEach()non è solo a scopo di debug .

Per me quando si sceglie tra forEach()e peek(), è scegliere quanto segue: Voglio parti di codice che mutano gli oggetti stream da allegare a un compostabile o voglio che si colleghino direttamente allo stream?

Penso peek()che abbinerà meglio ai metodi java9. ad esempio takeWhile()potrebbe essere necessario decidere quando interrompere l'iterazione in base a un oggetto già mutato, quindi abbinandoloforEach() avrebbe lo stesso effetto.

PS Non ho fatto riferimento da map()nessuna parte perché nel caso in cui vogliamo mutare oggetti (o stato globale), piuttosto che generare nuovi oggetti, funziona esattamente come peek().


3

Anche se sono d'accordo con la maggior parte delle risposte sopra, ho un caso in cui l'uso della sbirciata in realtà sembra il modo più pulito di procedere.

Simile al tuo caso d'uso, supponi di voler filtrare solo su account attivi e quindi eseguire un accesso su questi account.

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

Peek è utile per evitare la chiamata ridondante senza dover ripetere due volte la raccolta:

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

3
Tutto quello che devi fare è ottenere il metodo di accesso corretto. Non vedo davvero come la sbirciatina sia la strada più pulita da percorrere. In che modo qualcuno che sta leggendo il tuo codice dovrebbe sapere che stai effettivamente abusando dell'API. Un codice valido e pulito non obbliga un lettore a fare ipotesi sul codice.
kaba713,

1

La soluzione funzionale è rendere immutabile l'oggetto account. Quindi account.login () deve restituire un nuovo oggetto account. Ciò significa che l'operazione mappa può essere utilizzata per l'accesso anziché per sbirciatina.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.