Java 8 - Najlepszy sposób na przekształcenie listy: mapa czy foreach?

188

Mam listę, w myListToParsektórej chcę filtrować elementy, zastosować metodę do każdego elementu i dodać wynik do innej listy myFinalList.

W Javie 8 zauważyłem, że mogę to zrobić na 2 różne sposoby. Chciałbym poznać bardziej skuteczny sposób między nimi i zrozumieć, dlaczego jeden sposób jest lepszy od drugiego.

Jestem otwarty na wszelkie sugestie dotyczące trzeciego sposobu.

Metoda 1:

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

Metoda 2:

myFinalList = myListToParse.stream()
        .filter(elt -> elt != null)
        .map(elt -> doSomething(elt))
        .collect(Collectors.toList()); 
Emilien Brigand
źródło
55
Drugie. Właściwa funkcja nie powinna mieć skutków ubocznych, w pierwszej implementacji modyfikujesz świat zewnętrzny.
ThanksForAllTheFish
37
tylko kwestia stylu, ale elt -> elt != nullmożna go zastąpićObjects::nonNull
the8472
2
@ the8472 Jeszcze lepiej byłoby upewnić się, że w kolekcji nie ma żadnych wartości null, i Optional<T>zamiast tego używać w połączeniu z flatMap.
herman
2
@SzymonRoziewski, niezupełnie. W przypadku czegoś tak trywialnego jak to, praca potrzebna do ustawienia równoległego strumienia pod maską sprawi, że użycie tego konstruktu wyciszy.
MK
2
Pamiętaj, że możesz pisać, .map(this::doSomething)zakładając, że doSomethingjest to metoda niestatyczna. Jeśli jest statyczny, możesz go zastąpić thisnazwą klasy.
herman

Odpowiedzi:

153

Nie martw się o różnice w wydajności, zwykle będą minimalne w tym przypadku.

Preferowana jest metoda 2, ponieważ

  1. nie wymaga mutowania kolekcji, która istnieje poza wyrażeniem lambda,

  2. jest bardziej czytelny, ponieważ różne kroki wykonywane w potoku gromadzenia są zapisywane sekwencyjnie: najpierw operacja filtrowania, następnie operacja mapowania, a następnie zbieranie wyniku (więcej informacji na temat korzyści płynących z potoków zbierania znajduje się w doskonałym artykule Martina Fowlera ),

  3. możesz łatwo zmienić sposób zbierania wartości, zastępując Collectorużywany. W niektórych przypadkach może być konieczne napisanie własnego Collector, ale korzyścią jest to, że można go łatwo ponownie użyć.

herman
źródło
43

Zgadzam się z istniejącymi odpowiedziami, że druga forma jest lepsza, ponieważ nie ma żadnych skutków ubocznych i jest łatwiejsza do zrównoleglenia (wystarczy użyć strumienia równoległego).

Jeśli chodzi o wydajność, wydaje się, że są one równoważne, dopóki nie zaczniesz używać równoległych strumieni. W takim przypadku mapa będzie działać znacznie lepiej. Zobacz poniżej wyniki testu porównawczego mikro :

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

Nie możesz wzmocnić pierwszego przykładu w ten sam sposób, ponieważ forEach jest metodą terminalną - zwraca void - więc musisz użyć stanowej lambda. Ale to naprawdę zły pomysł, jeśli używasz równoległych strumieni .

Na koniec zauważ, że Twój drugi fragment kodu można napisać w nieco bardziej zwięzły sposób za pomocą odwołań do metod i importu statycznego:

myFinalList = myListToParse.stream()
    .filter(Objects::nonNull)
    .map(this::doSomething)
    .collect(toList()); 
assylias
źródło
1
Jeśli chodzi o wydajność, w twoim przypadku „mapa” naprawdę wygrywa z „forEach”, jeśli używasz równoległych strumieni. Moje wyniki w milisekundach: SO28319064. Dla każdego: 187,310 ± 1768 ms / operacja - SO28319064.map: 189,180 ± 1,692 ms / operacja - SO28319064.mapParallelStream: 55,577 ± 0,782 ms / operacja
Giuseppe Bertone
2
@GiuseppeBertone, to zależy od assylias, ale moim zdaniem twoja edycja jest sprzeczna z intencjami autora. Jeśli chcesz dodać własną odpowiedź, lepiej ją dodać zamiast tak bardzo edytować istniejącą. Również teraz link do mikrodruku nie ma znaczenia dla wyników.
Tagir Valeev
5

Jedną z głównych zalet korzystania ze strumieni jest to, że daje możliwość przetwarzania danych w sposób deklaratywny, to znaczy przy użyciu funkcjonalnego stylu programowania. Daje także możliwość wielowątkowości za darmo, co oznacza, że ​​nie trzeba pisać żadnego dodatkowego kodu wielowątkowego, aby strumień był współbieżny.

Zakładając, że odkrywasz ten styl programowania, ponieważ chcesz wykorzystać te zalety, wtedy Twoja pierwsza próbka kodu potencjalnie nie działa, ponieważ foreachmetoda jest sklasyfikowana jako terminalna (co oznacza, że ​​może wywoływać skutki uboczne).

Drugi sposób jest preferowany z punktu widzenia programowania funkcjonalnego, ponieważ funkcja mapy może przyjmować bezstanowe funkcje lambda. Mówiąc dokładniej, lambda przekazywana do funkcji mapy powinna być

  1. Nie przeszkadza, co oznacza, że ​​funkcja nie powinna zmieniać źródła strumienia, jeśli nie jest współbieżny (np ArrayList.).
  2. Bezstanowy, aby uniknąć nieoczekiwanych wyników podczas przetwarzania równoległego (spowodowanego różnicami planowania wątków).

Inną korzyścią z drugiego podejścia jest to, że jeśli strumień jest równoległy, a kolektor jest współbieżny i nieuporządkowany, wówczas te cechy mogą dostarczyć użytecznych wskazówek dotyczących operacji redukcji w celu jednoczesnego zbierania.

MK
źródło
4

Jeśli korzystasz z kolekcji Eclipse , możesz użyć tej collectIf()metody.

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);

Ocenia z niecierpliwością i powinien być nieco szybszy niż przy użyciu Strumienia.

Uwaga: jestem osobą odpowiedzialną za kolekcje Eclipse.

Craig P. Motlin
źródło
1

Wolę drugi sposób.

Jeśli użyjesz pierwszego sposobu, jeśli zdecydujesz się użyć strumienia równoległego w celu poprawy wydajności, nie będziesz mieć kontroli nad kolejnością dodawania elementów do listy wyników forEach.

Podczas korzystania toListz interfejsu API Streams zachowa kolejność, nawet jeśli używasz strumienia równoległego.

Eran
źródło
Nie jestem pewien, czy to właściwa rada: mógłby użyć forEachOrderedzamiast tego, forEachgdyby chciał użyć strumienia równoległego, ale nadal zachował porządek. Ale jako dokumentacja forEachstanów, zachowanie porządku spotkań poświęca korzyść z równoległości. Podejrzewam, że tak też jest w tym przypadku toList.
herman
0

Istnieje trzecia opcja - użycie stream().toArray()- zobacz komentarze poniżej, dlaczego stream nie ma metody toList . Okazuje się, że jest wolniejszy niż forEach () lub collect () i mniej wyrazisty. Może być zoptymalizowany w późniejszych wersjach JDK, więc dodaj go tutaj na wszelki wypadek.

zarozumiały List<String>

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

z testem mikro-mikro, wpisami 1M, zerami 20% i prostą transformacją w 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;
}

wyniki są

równolegle:

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}

sekwencyjny:

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}

równolegle bez zer i filtru (więc strumień jest SIZED): toArrays ma najlepszą wydajność w takim przypadku i .forEach()kończy się niepowodzeniem z „indexOutOfBounds” na odbiorniku ArrayList, musiał zastąpić przez.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}
harshtuna
źródło
0

Może być metoda 3.

Zawsze wolę oddzielić logikę.

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());
Kumar Abhishek
źródło
0

W przypadku korzystania 3-ty Pary bibliotekami jest ok Cyklop reagują definiuje Lazy rozszerzyć zbiory z tej funkcji wbudowanych w. Na przykład moglibyśmy po prostu napisać

ListX myListToParse;

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

myFinalList nie jest oceniany aż do pierwszego dostępu (i tam po zmaterializowaniu listy i buforowaniu i ponownym użyciu).

[Ujawnienie Jestem wiodącym twórcą Cyclops-reag]

John McClean
źródło