Java 8 – Beste manier om een ​​lijst te transformeren: map of foreach?

Ik heb een lijst myListToParsewaar ik de elementen wil filteren en een methode wil toepassen op elk element, en het resultaat wil toevoegen aan een andere lijst myFinalList.

Met Java 8 merkte ik dat ik het op 2 verschillende manieren kan doen. Ik zou graag de efficiëntere weg tussen hen willen weten en begrijpen waarom de ene manier beter is dan de andere.

Ik sta open voor elke suggestie over een derde manier.

Methode 1:

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

Methode 2:

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

Antwoord 1, autoriteit 100%

Maak je geen zorgen over prestatieverschillen, normaal gesproken zullen ze in dit geval minimaal zijn.

Methode 2 verdient de voorkeur omdat

  1. het vereist geen mutatie van een verzameling die buiten de lambda-expressie bestaat.

  2. het is beter leesbaar omdat de verschillende stappen die worden uitgevoerd in de verzamelingspijplijn opeenvolgend worden geschreven: eerst een filterbewerking, dan een kaartbewerking en vervolgens het resultaat verzamelen (voor meer informatie over de voordelen van verzamelingspijplijnen, zie uitstekend artikelvan Martin Fowler.)

  3. je kunt de manier waarop waarden worden verzameld eenvoudig wijzigen door de Collectordie wordt gebruikt te vervangen. In sommige gevallen moet je misschien je eigen Collectorschrijven, maar het voordeel is dat je die gemakkelijk opnieuw kunt gebruiken.


Antwoord 2, autoriteit 28%

Ik ben het eens met de bestaande antwoorden dat de tweede vorm beter is omdat het geen bijwerkingen heeft en gemakkelijker te parallelliseren is (gebruik gewoon een parallelle stroom).

Wat de prestaties betreft, lijkt het erop dat ze gelijkwaardig zijn totdat je parallelle streams gaat gebruiken. In dat geval zal mapecht veel beter presteren. Zie hieronder de microbenchmarkresultaten:

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

Je kunt het eerste voorbeeld niet op dezelfde manier boosten omdat forEacheen terminalmethode is – het geeft void terug – dus je bent gedwongen een stateful lambda te gebruiken. Maar dat is echt een slecht idee als je parallelle streams gebruikt.

Ten slotte moet u er rekening mee houden dat uw tweede fragment op een iets beknoptere manier kan worden geschreven met verwijzingen naar methoden en statische invoer:

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

Antwoord 3, autoriteit 3%

Als u Eclipse Collectionsgebruikt, kunt u de collectIf()methode.

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

Het evalueert gretig en zou een beetje sneller moeten zijn dan het gebruik van een Stream.

Opmerking:ik ben toegewijd aan Eclipse Collections.


Antwoord 4, autoriteit 3%

Een van de belangrijkste voordelen van het gebruik van streams is dat het de mogelijkheid biedt om gegevens op een declaratieve manier te verwerken, dat wil zeggen met een functionele programmeerstijl. Het biedt ook gratis multi-threading mogelijkheden, wat betekent dat het niet nodig is om extra multi-threaded code te schrijven om uw stream gelijktijdig te maken.

Ervan uitgaande dat u deze programmeerstijl verkent, is dat u deze voordelen wilt benutten, dan is uw eerste codevoorbeeld mogelijk niet functioneel, aangezien de forEach-methode wordt geclassificeerd als terminal (wat betekent dat het bijwerkingen kunnen veroorzaken).

De tweede manier heeft de voorkeur vanuit het oogpunt van functioneel programmeren, aangezien de kaartfunctie toestandloze lambda-functies kan accepteren. Meer expliciet, de lambda die aan de kaartfunctie wordt doorgegeven, moet zijn

  1. Niet-interfererend, wat betekent dat de functie de bron van de stream niet mag wijzigen als deze niet-gelijktijdig is (bijv. ArrayList).
  2. Statloos om onverwachte resultaten te voorkomen bij parallelle verwerking (veroorzaakt door verschillen in threadplanning).

Een ander voordeel van de tweede benadering is dat als de stroom parallel is en de collector gelijktijdig en ongeordend is, deze kenmerken nuttige hints kunnen geven voor de reductiebewerking om het verzamelen tegelijkertijd uit te voeren.


Antwoord 5

Ik geef de voorkeur aan de tweede manier.

Als je de eerste manier gebruikt en je besluit een parallelle stream te gebruiken om de prestaties te verbeteren, heb je geen controle over de volgorde waarin de elementen worden toegevoegd aan de uitvoerlijst door forEach.

Als u toListgebruikt, behoudt de Streams API de volgorde, zelfs als u een parallelle stream gebruikt.


Antwoord 6

Er is een derde optie – met behulp van stream().toArray()– zie opmerkingen onder waarom had stream geen toList-methode. Het blijkt langzamer te zijn dan forEach() of collect(), en minder expressief. Het kan worden geoptimaliseerd in latere JDK-builds, dus voeg het hier toe voor het geval dat.

ervan uitgaande dat List<String>

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

met een micro-microbenchmark, 1 miljoen inzendingen, 20% nulls en eenvoudige transformatie 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;
}

de resultaten zijn

parallel:

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}

sequentieel:

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}

parallel zonder nulls en filter (dus de stream is SIZED):
toArrays presteert in dat geval het beste, en .forEach()faalt met “indexOutOfBounds” op de ontvangende ArrayList, moest vervangen worden door .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}

Antwoord 7

Als het gebruik van 3rd Pary Libaries oké is, definieert cyclops-reactLazy uitgebreide collecties met deze functionaliteit ingebouwd. We kunnen bijvoorbeeld gewoon

. schrijven

ListX myListToParse;

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

myFinalList wordt niet geëvalueerd tot de eerste toegang (en daarna wordt de gemaakte lijst in de cache opgeslagen en opnieuw gebruikt).

[Disclosure Ik ben de hoofdontwikkelaar van cyclops-react]


Antwoord 8

Misschien methode 3.

Ik hou de logica altijd liever gescheiden.

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

Other episodes