Java 8 - Il modo migliore per trasformare un elenco: mappa o foreach?


188

Ho un elenco in myListToParsecui voglio filtrare gli elementi e applicare un metodo su ciascun elemento e aggiungere il risultato in un altro elenco myFinalList.

Con Java 8 ho notato che posso farlo in 2 modi diversi. Vorrei sapere il modo più efficiente tra loro e capire perché un modo è migliore dell'altro.

Sono aperto per qualsiasi suggerimento su una terza via.

Metodo 1:

myFinalList = new ArrayList<>();
myListToParse.stream()
        .filter(elt -> elt != null)
        .forEach(elt -> myFinalList.add(doSomething(elt)));

Metodo 2:

myFinalList = myListToParse.stream()
        .filter(elt -> elt != null)
        .map(elt -> doSomething(elt))
        .collect(Collectors.toList()); 

55
Il secondo. Una funzione adeguata non dovrebbe avere effetti collaterali, nella tua prima implementazione stai modificando il mondo esterno.
ThanksForAllTheFish,

37
solo una questione di stile, ma elt -> elt != nullpuò essere sostituito conObjects::nonNull
the8472

2
@ the8472 Ancora meglio sarebbe assicurarsi che non ci siano valori nulli nella raccolta in primo luogo, e utilizzare Optional<T>invece in combinazione con flatMap.
herman,

2
@SzymonRoziewski, non proprio. Per qualcosa di così banale, il lavoro necessario per impostare il parallelo sotto il cofano renderà muto l'uso di questo costrutto.
MK

2
Nota che puoi scrivere .map(this::doSomething)supponendo che doSomethingsia un metodo non statico. Se è statico, puoi sostituirlo thiscon il nome della classe.
herman,

Risposte:


153

Non preoccuparti delle differenze di prestazioni, in questo caso saranno minime in questo caso.

Il metodo 2 è preferibile perché

  1. non richiede la mutazione di una raccolta che esiste al di fuori dell'espressione lambda,

  2. è più leggibile perché i diversi passaggi eseguiti nella pipeline di raccolta sono scritti in sequenza: prima un'operazione di filtro, quindi un'operazione di mappa, quindi la raccolta del risultato (per ulteriori informazioni sui vantaggi delle pipeline di raccolta, vedere l' articolo eccellente di Martin Fowler ),

  3. puoi facilmente cambiare il modo in cui i valori vengono raccolti sostituendo Collectorquello utilizzato. In alcuni casi potresti dover scrivere il tuo Collector, ma il vantaggio è che puoi riutilizzarlo facilmente.


43

Concordo con le risposte esistenti sul fatto che la seconda forma è migliore perché non ha effetti collaterali ed è più facile parallelizzare (basta usare un flusso parallelo).

Per quanto riguarda le prestazioni, sembra che siano equivalenti fino a quando non inizi a utilizzare flussi paralleli. In tal caso, la mappa funzionerà molto meglio. Vedi sotto i risultati del micro benchmark :

Benchmark                         Mode  Samples    Score   Error  Units
SO28319064.forEach                avgt      100  187.310 ± 1.768  ms/op
SO28319064.map                    avgt      100  189.180 ± 1.692  ms/op
SO28319064.mapWithParallelStream  avgt      100   55,577 ± 0,782  ms/op

Non puoi potenziare il primo esempio nello stesso modo perché forEach è un metodo terminale - restituisce nulla - quindi sei costretto a usare un lambda con stato. Ma questa è davvero una cattiva idea se si utilizzano flussi paralleli .

Infine, nota che il tuo secondo frammento può essere scritto in modo leggermente più conciso con riferimenti a metodi e importazioni statiche:

myFinalList = myListToParse.stream()
    .filter(Objects::nonNull)
    .map(this::doSomething)
    .collect(toList()); 

1
Per quanto riguarda le prestazioni, nel tuo caso "map" conquista davvero "forEach" se usi parallelStreams. Le mie panchine in millisecondi: SO28319064.per Ogni: 187.310 ± 1.768 ms / op - SO28319064.map: 189.180 ± 1.692 ms / op --SO28319064.mapParallelStream: 55.577 ± 0.782 ms / op
Giuseppe Bertone

2
@GiuseppeBertone, dipende da Assylias, ma a mio avviso la tua modifica contraddice l'intento dell'autore originale. Se vuoi aggiungere la tua risposta, è meglio aggiungerla invece di modificare quella esistente così tanto. Anche ora il collegamento al microbenchmark non è rilevante per i risultati.
Tagir Valeev,

5

Uno dei principali vantaggi dell'utilizzo dei flussi è che offre la possibilità di elaborare i dati in modo dichiarativo, ovvero utilizzando uno stile funzionale di programmazione. Offre inoltre funzionalità multi-threading per un significato gratuito, non è necessario scrivere alcun codice multi-thread aggiuntivo per rendere simultaneo il flusso.

Supponendo che il motivo per cui stai esplorando questo stile di programmazione sia che vuoi sfruttare questi vantaggi, il tuo primo esempio di codice non è potenzialmente funzionale poiché il foreachmetodo è classificato come terminale (nel senso che può produrre effetti collaterali).

Il secondo modo è preferito dal punto di vista della programmazione funzionale poiché la funzione mappa può accettare funzioni lambda senza stato. Più esplicitamente, dovrebbe essere la lambda passata alla funzione mappa

  1. Non interferisce, nel senso che la funzione non dovrebbe alterare la sorgente del flusso se non è simultanea (ad es ArrayList.).
  2. Stateless per evitare risultati imprevisti durante l'elaborazione parallela (causata da differenze nella pianificazione dei thread).

Un altro vantaggio con il secondo approccio è se il flusso è parallelo e il collettore è simultaneo e non ordinato, quindi queste caratteristiche possono fornire utili suggerimenti all'operazione di riduzione per effettuare la raccolta contemporaneamente.


4

Se si utilizzano le raccolte Eclipse è possibile utilizzare il collectIf()metodo

MutableList<Integer> source =
    Lists.mutable.with(1, null, 2, null, 3, null, 4, null, 5);

MutableList<String> result = source.collectIf(Objects::nonNull, String::valueOf);

Assert.assertEquals(Lists.immutable.with("1", "2", "3", "4", "5"), result);

Valuta con entusiasmo e dovrebbe essere un po 'più veloce rispetto all'utilizzo di un flusso.

Nota: sono un committer per le raccolte Eclipse.


1

Preferisco il secondo modo.

Quando si utilizza il primo modo, se si decide di utilizzare un flusso parallelo per migliorare le prestazioni, non si avrà alcun controllo sull'ordine in cui gli elementi verranno aggiunti all'elenco di output per forEach.

Quando si utilizza toList, l'API Streams conserva l'ordine anche se si utilizza un flusso parallelo.


Non sono sicuro che questo sia un consiglio corretto: potrebbe usare forEachOrderedinvece di forEachse volesse usare un flusso parallelo ma preservare comunque l'ordine. Ma poiché la documentazione per gli forEachstati, preservare l'ordine dell'incontro sacrifica il beneficio del parallelismo. Ho il sospetto che anche in questo caso toList.
herman,

0

C'è una terza opzione - utilizzo stream().toArray()- vedere i commenti sotto perché lo streaming non ha un metodo toList . Risulta essere più lento di forEach () o collect () e meno espressivo. Potrebbe essere ottimizzato nelle versioni successive di JDK, quindi aggiungendolo qui per ogni evenienza.

supponendo List<String>

    myFinalList = Arrays.asList(
            myListToParse.stream()
                    .filter(Objects::nonNull)
                    .map(this::doSomething)
                    .toArray(String[]::new)
    );

con un micro-micro benchmark, voci 1M, null del 20% e trasformazione semplice in doSomething ()

private LongSummaryStatistics benchmark(final String testName, final Runnable methodToTest, int samples) {
    long[] timing = new long[samples];
    for (int i = 0; i < samples; i++) {
        long start = System.currentTimeMillis();
        methodToTest.run();
        timing[i] = System.currentTimeMillis() - start;
    }
    final LongSummaryStatistics stats = Arrays.stream(timing).summaryStatistics();
    System.out.println(testName + ": " + stats);
    return stats;
}

i risultati sono

parallelo:

toArray: LongSummaryStatistics{count=10, sum=3721, min=321, average=372,100000, max=535}
forEach: LongSummaryStatistics{count=10, sum=3502, min=249, average=350,200000, max=389}
collect: LongSummaryStatistics{count=10, sum=3325, min=265, average=332,500000, max=368}

sequenziale:

toArray: LongSummaryStatistics{count=10, sum=5493, min=517, average=549,300000, max=569}
forEach: LongSummaryStatistics{count=10, sum=5316, min=427, average=531,600000, max=571}
collect: LongSummaryStatistics{count=10, sum=5380, min=444, average=538,000000, max=557}

parallelo senza null e filtro (quindi lo stream è SIZED): toArrays ha le migliori prestazioni in questo caso e .forEach()non riesce con "indexOutOfBounds" sull'ArrayList dei destinatari, ha dovuto sostituire con.forEachOrdered()

toArray: LongSummaryStatistics{count=100, sum=75566, min=707, average=755,660000, max=1107}
forEach: LongSummaryStatistics{count=100, sum=115802, min=992, average=1158,020000, max=1254}
collect: LongSummaryStatistics{count=100, sum=88415, min=732, average=884,150000, max=1014}

0

Può essere il metodo 3.

Preferisco sempre mantenere la logica separata.

Predicate<Long> greaterThan100 = new Predicate<Long>() {
            @Override
            public boolean test(Long currentParameter) {
                return currentParameter > 100;
            }
        };

        List<Long> sourceLongList = Arrays.asList(1L, 10L, 50L, 80L, 100L, 120L, 133L, 333L);
        List<Long> resultList = sourceLongList.parallelStream().filter(greaterThan100).collect(Collectors.toList());

0

Se l'utilizzo di 3rd Pary Libaries è ok cyclops- reazioni definisce raccolte estese pigre con questa funzionalità integrata. Ad esempio, potremmo semplicemente scrivere

ListX myListToParse;

ListX myFinalList = myListToParse.filter (elt -> elt! = Null) .map (elt -> doSomething (elt));

myFinalList non viene valutato fino al primo accesso (e lì dopo che l'elenco materializzato viene memorizzato nella cache e riutilizzato).

[Divulgazione Sono lo sviluppatore principale di cyclops-reagire]

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.