Operazioni di flusso intermedio non valutate al conteggio


33

Sembra che non riesca a capire come Java componga le operazioni dello stream in una pipeline dello stream.

Quando si esegue il seguente codice

public
 static void main(String[] args) {
    StringBuilder sb = new StringBuilder();

    var count = Stream.of(new String[]{"1", "2", "3", "4"})
            .map(sb::append)
            .count();

    System.out.println(count);
    System.out.println(sb.toString());
}

La console stampa solo 4. L' StringBuilderoggetto ha ancora il valore "".

Quando aggiungo l'operazione di filtro: filter(s -> true)

public static void main(String[] args) {
    StringBuilder sb = new StringBuilder();

    var count = Stream.of(new String[]{"1", "2", "3", "4"})
            .filter(s -> true)
            .map(sb::append)
            .count();

    System.out.println(count);
    System.out.println(sb.toString());
}

L'output cambia in:

4
1234

In che modo questa operazione di filtro apparentemente ridondante modifica il comportamento della pipeline del flusso composta?


2
Interessante !!!
uneq95,

3
Immagino che questo sia un comportamento specifico dell'implementazione; forse è perché il primo flusso ha una dimensione nota, ma il secondo no, e la dimensione determina se le operazioni intermedie vengono eseguite.
Andy Turner,

Per interesse, cosa succede se inverti il ​​filtro e la mappa?
Andy Turner,

Avendo programmato un po 'in Haskell, puzza un po' come una valutazione pigra in corso qui. È tornata una ricerca su Google, che i flussi hanno davvero una pigrizia. Potrebbe essere così? E senza filtro, se java è abbastanza intelligente, non è necessario eseguire effettivamente la mappatura.
Frederik,

@AndyTurner Dà lo stesso risultato, anche su inversione
uneq95

Risposte:


39

L' count()operazione terminale, nella mia versione di JDK, finisce per eseguire il seguente codice:

if (StreamOpFlag.SIZED.isKnown(helper.getStreamAndOpFlags()))
    return spliterator.getExactSizeIfKnown();
return super.evaluateSequential(helper, spliterator);

Se è presente filter()un'operazione nella pipeline di operazioni, la dimensione del flusso, inizialmente nota, non può più essere nota (poiché filterpotrebbe rifiutare alcuni elementi del flusso). Quindi il ifblocco non viene eseguito, le operazioni intermedie vengono eseguite e StringBuilder viene così modificato.

D'altra parte, se hai solo map()nella pipeline, il numero di elementi nel flusso è garantito essere uguale al numero iniziale di elementi. Quindi il blocco if viene eseguito e la dimensione viene restituita direttamente senza valutare le operazioni intermedie.

Si noti che la lambda è passata per map()violare il contratto definito nella documentazione: si suppone che sia un'operazione senza interferenze, senza stato, ma non è senza stato. Quindi avere un risultato diverso in entrambi i casi non può essere considerato un bug.


Perché flatMap()potrebbe essere in grado di cambiare il numero di elementi, era questo il motivo per cui inizialmente era desideroso (ora pigro)? Quindi, l'alternativa sarebbe usare forEach()e contare separatamente se, map()nella sua forma attuale, viola il contratto, immagino.
Frederik,

3
Per quanto riguarda flatMap, non credo. Era, AFAIK, perché inizialmente era più semplice renderlo desideroso. Sì, usare uno stream, con map (), per produrre effetti collaterali è una cattiva idea.
JB Nizet,

Avresti un suggerimento su come ottenere l'output completo 4 1234senza utilizzare il filtro aggiuntivo o produrre effetti collaterali nell'operazione map ()?
Atalanto

1
int count = array.length; String result = String.join("", array);
JB Nizet,

1
o potresti usare forOach se vuoi davvero usare StringBuilder, oppure puoi usareCollectors.joining("")
njzk2

19

In jdk-9 era chiaramente documentato nei documenti java

Anche l'eliminazione degli effetti collaterali può essere sorprendente. Ad eccezione delle operazioni terminali per Each e forEachOrdered, gli effetti collaterali dei parametri comportamentali potrebbero non essere sempre eseguiti quando l'implementazione del flusso può ottimizzare l'esecuzione dei parametri comportamentali senza influire sul risultato del calcolo. (Per un esempio specifico, consultare la nota API documentata sull'operazione di conteggio .)

Nota API:

Un'implementazione può scegliere di non eseguire la pipeline del flusso (in sequenza o in parallelo) se è in grado di calcolare il conteggio direttamente dalla sorgente del flusso. In tali casi, nessun elemento sorgente verrà attraversato e nessuna operazione intermedia verrà valutata. I parametri comportamentali con effetti collaterali, che sono fortemente scoraggiati, ad eccezione di casi innocui come il debug, possono essere interessati. Ad esempio, considera il seguente flusso:

 List<String> l = Arrays.asList("A", "B", "C", "D");
 long count = l.stream().peek(System.out::println).count();

Il numero di elementi coperti dalla sorgente del flusso, un elenco, è noto e l'operazione intermedia, peek, non inserisce o rimuove elementi dal flusso (come potrebbe essere il caso delle operazioni flatMap o del filtro). Pertanto il conteggio è la dimensione dell'elenco e non è necessario eseguire la pipeline e, come effetto collaterale, stampare gli elementi dell'elenco.


0

Questo non è lo scopo di .map. Dovrebbe essere usato per trasformare un flusso di "Something" in un flusso di "Something Else". In questo caso, stai usando map per aggiungere una stringa a un Stringbuilder esterno, dopo di che hai un flusso di "Stringbuilder", ognuno dei quali è stato creato dall'operazione map aggiungendo un numero al Stringbuilder originale.

Il tuo flusso in realtà non fa nulla con i risultati mappati nel flusso, quindi è perfettamente ragionevole supporre che il passaggio possa essere ignorato dal processore del flusso. Conta sugli effetti collaterali per fare il lavoro, che rompe il modello funzionale della mappa. Saresti meglio servito usando forOach per fare questo. Esegui il conteggio come un flusso separato o inserisci un contatore utilizzando AtomicInt in forEach.

Il filtro lo costringe a eseguire i contenuti dello stream poiché ora deve fare qualcosa di sensibilmente significativo con ogni elemento dello stream.

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.