Quando dovrei usare gli stream?


99

Ho appena trovato una domanda durante l'utilizzo di a Liste il suo stream()metodo. Anche se so come usarli, non sono abbastanza sicuro di quando usarli.

Ad esempio, ho un elenco, contenente vari percorsi verso posizioni diverse. Ora, vorrei verificare se un singolo percorso specificato contiene uno dei percorsi specificati nell'elenco. Vorrei restituire un in booleanbase al fatto che la condizione sia stata soddisfatta o meno.

Questo ovviamente non è un compito difficile di per sé. Ma mi chiedo se dovrei usare flussi o un ciclo for (-each).

La lista

private static final List<String> EXCLUDE_PATHS = Arrays.asList(new String[]{
    "my/path/one",
    "my/path/two"
});

Esempio: Stream

private boolean isExcluded(String path){
    return EXCLUDE_PATHS.stream()
                        .map(String::toLowerCase)
                        .filter(path::contains)
                        .collect(Collectors.toList())
                        .size() > 0;
}

Esempio: ciclo For-Each

private boolean isExcluded(String path){
    for (String excludePath : EXCLUDE_PATHS) {
        if(path.contains(excludePath.toLowerCase())){
            return true;
        }
    }
    return false;
}

Notare che il pathparametro è sempre minuscolo .

La mia prima ipotesi è che l'approccio for-each sia più veloce, perché il ciclo tornerebbe immediatamente, se la condizione è soddisfatta. Mentre il flusso continua a scorrere tutte le voci dell'elenco per completare il filtraggio.

La mia ipotesi è corretta? Se è così, perché (o meglio quando ) dovrei usare stream()allora?


11
I flussi sono più espressivi e leggibili dei tradizionali cicli for. In seguito devi stare attento agli intrinseci di if-then e alle condizioni, ecc. L'espressione del flusso è molto chiara: converti i nomi dei file in minuscole, quindi filtra per qualcosa e poi conta, raccogli, ecc. Il risultato: molto iterativo espressione del flusso di calcoli.
Jean-Baptiste Yunès

12
Non c'è bisogno di new String[]{…}qui. Basta usareArrays.asList("my/path/one", "my/path/two")
Holger

4
Se la tua fonte è a String[], non è necessario chiamare Arrays.asList. Puoi semplicemente eseguire lo streaming sull'array usando Arrays.stream(array). A proposito, ho difficoltà a capire del tutto lo scopo del isExcludedtest. È davvero interessante se un elemento di EXCLUDE_PATHSè contenuto letteralmente da qualche parte all'interno di path? Cioè isExcluded("my/path/one/foo/bar/baz")torneremo true, così come isExcluded("foo/bar/baz/my/path/one/")...
Holger

3
Fantastico, non ero a conoscenza del Arrays.streammetodo, grazie per averlo fatto notare. In effetti, l'esempio che ho postato sembra abbastanza inutile per chiunque altro oltre a me. Sono consapevole del comportamento del isExcludedmetodo, ma in realtà è solo qualcosa di cui ho bisogno per me stesso, quindi, per rispondere alla tua domanda: , è interessante per ragioni che vorrei non menzionare, in quanto non rientrerebbe nello scopo della domanda originale.
mcuenez

1
Perché viene toLowerCaseapplicata alla costante che è già minuscola? Non dovrebbe essere applicato pathall'argomento?
Sebastian Redl

Risposte:


78

La tua ipotesi è corretta. L'implementazione del tuo flusso è più lenta del ciclo for.

Questo utilizzo del flusso dovrebbe essere veloce quanto il ciclo for:

EXCLUDE_PATHS.stream()  
                               .map(String::toLowerCase)
                               .anyMatch(path::contains);

Questo itera attraverso gli elementi, applicando String::toLowerCasee il filtro agli elementi uno per uno e terminando al primo elemento che corrisponde.

Entrambe collect()& anyMatch()sono operazioni del terminale. anyMatch()esce dal primo elemento trovato, tuttavia, collect()richiede l'elaborazione di tutti gli elementi.


2
Fantastico, non sapevo findFirst()in combinazione con filter(). A quanto pare, non so come usare i flussi così come pensavo.
mcuenez

4
Ci sono alcuni articoli e presentazioni di blog davvero interessanti sul Web sulle prestazioni delle API di flusso, che ho trovato molto utili per capire come funziona questa roba sotto il cofano. Posso sicuramente consigliare solo di fare ricerche, se sei interessato a questo.
Stefan Pries,

Dopo la tua modifica, sento che la tua risposta è quella che dovrebbe essere accettata, poiché hai anche risposto alla mia domanda nei commenti dell'altra risposta. Tuttavia, vorrei dare a @ rvit34 un po 'di credito per aver pubblicato il codice :-)
mcuenez

34

La decisione se utilizzare Streams o meno non dovrebbe essere guidata dalla considerazione delle prestazioni, ma piuttosto dalla leggibilità. Quando si tratta davvero di prestazioni, ci sono altre considerazioni.

Con il tuo .filter(path::contains).collect(Collectors.toList()).size() > 0approccio, stai elaborando tutti gli elementi e raccogliendoli in modo temporaneo List, prima di confrontare le dimensioni, tuttavia, questo non importa quasi mai per uno Stream composto da due elementi.

L'utilizzo .map(String::toLowerCase).anyMatch(path::contains)può far risparmiare cicli di CPU e memoria, se si dispone di un numero sostanzialmente maggiore di elementi. Tuttavia, questo converte ciascuno Stringnella sua rappresentazione minuscola, finché non viene trovata una corrispondenza. Ovviamente, ha senso usare

private static final List<String> EXCLUDE_PATHS =
    Stream.of("my/path/one", "my/path/two").map(String::toLowerCase)
          .collect(Collectors.toList());

private boolean isExcluded(String path) {
    return EXCLUDE_PATHS.stream().anyMatch(path::contains);
}

anziché. Quindi non è necessario ripetere la conversione in minuscolo in ogni chiamata di isExcluded. Se il numero di elementi EXCLUDE_PATHSo la lunghezza delle stringhe diventa molto grande, potresti prendere in considerazione l'utilizzo di

private static final List<Predicate<String>> EXCLUDE_PATHS =
    Stream.of("my/path/one", "my/path/two").map(String::toLowerCase)
          .map(s -> Pattern.compile(s, Pattern.LITERAL).asPredicate())
          .collect(Collectors.toList());

private boolean isExcluded(String path){
    return EXCLUDE_PATHS.stream().anyMatch(p -> p.test(path));
}

Compilare una stringa come pattern regex con il LITERALflag, fa sì che si comporti come le normali operazioni sulle stringhe, ma consente al motore di dedicare un po 'di tempo alla preparazione, ad esempio utilizzando l'algoritmo di Boyer Moore, per essere più efficiente quando si tratta del confronto effettivo.

Naturalmente, questo ripaga solo se ci sono abbastanza test successivi per compensare il tempo speso nella preparazione. Determinare se questo sarà il caso, è una delle considerazioni sulle prestazioni effettive, oltre alla prima domanda se questa operazione sarà mai critica per le prestazioni. Non la questione se utilizzare Stream o forloop.

A proposito, gli esempi di codice sopra mantengono la logica del tuo codice originale, che mi sembra discutibile. Il tuo isExcludedmetodo restituisce true, se il percorso specificato contiene uno qualsiasi degli elementi nell'elenco, quindi restituisce trueper /some/prefix/to/my/path/one, così come my/path/one/and/some/suffixo anche /some/prefix/to/my/path/one/and/some/suffix.

Anche dummy/path/onerousè considerato soddisfare i criteri in quanto containsla stringa my/path/one...


Buoni approfondimenti sulla possibile ottimizzazione delle prestazioni, grazie. Per quanto riguarda l'ultima parte della tua risposta: se la mia risposta al tuo commento non è stata soddisfacente, considera il mio codice di esempio come un semplice aiuto per gli altri per capire quello che sto chiedendo, piuttosto che essere codice reale. Inoltre, puoi sempre modificare la domanda, se hai in mente un esempio migliore.
mcuenez

3
Prendo il tuo commento che questa operazione è quello che vuoi veramente, quindi non c'è bisogno di cambiarla. Conserverò l'ultima sezione per i futuri lettori, quindi sono consapevoli che questa non è un'operazione tipica, ma anche che è già stata discussa e non ha bisogno di ulteriori commenti ...
Holger

In realtà gli stream sono perfetti da utilizzare per l'ottimizzazione della memoria quando la quantità di memoria di lavoro
supera il

21

Si. Hai ragione. Il tuo approccio allo streaming avrà un sovraccarico. Ma puoi usare una tale costruzione:

private boolean isExcluded(String path) {
    return  EXCLUDE_PATHS.stream().map(String::toLowerCase).anyMatch(path::contains);
}

Il motivo principale per utilizzare i flussi è che rendono il codice più semplice e facile da leggere.


3
È anyMatchuna scorciatoia per filter(...).findFirst().isPresent()?
mcuenez

6
Sì! È anche meglio del mio primo suggerimento.
Stefan Pries,

8

L'obiettivo dei flussi in Java è semplificare la complessità della scrittura di codice parallelo. È ispirato alla programmazione funzionale. Il flusso seriale serve solo a rendere il codice più pulito.

Se vogliamo prestazioni dovremmo usare parallelStream, che è stato progettato per. Quello seriale, in generale, è più lento.

C'è un buon articolo di leggere su , e prestazioni . ForLoopStreamParallelStream

Nel tuo codice possiamo utilizzare metodi di terminazione per interrompere la ricerca alla prima corrispondenza. (anyMatch ...)


5
Tieni presente che per piccoli flussi e in alcuni altri casi, un flusso parallelo può essere più lento a causa del costo di avvio. E se si dispone di un'operazione terminale ordinata, piuttosto che parallelizzabile non ordinata, risincronizzazione alla fine.
CAD97

0

Come altri hanno già menzionato molti punti positivi, voglio solo menzionare la valutazione pigra nella valutazione del flusso. Quando map()creiamo un flusso di percorsi minuscoli, non creiamo immediatamente l'intero flusso, invece il flusso è costruito pigramente , motivo per cui le prestazioni dovrebbero essere equivalenti al tradizionale ciclo for. Non sta eseguendo una scansione completa map()e anyMatch()vengono eseguiti allo stesso tempo. Una volta anyMatch()restituito vero, verrà cortocircuitato.

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.