Java 8 - Bir listeyi dönüştürmenin en iyi yolu: harita mı yoksa foreach mi?


188

Ben bir liste var myListToParseben unsurları filtrelemek ve her öğe üzerinde bir yöntemi uygulamak ve başka bir listedeki bir sonuç eklemek istiyorum myFinalList.

Java 8 ile bunu 2 farklı şekilde yapabileceğimi fark ettim. Aralarındaki daha etkili yolu bilmek ve bir yolun neden diğerinden daha iyi olduğunu anlamak istiyorum.

Üçüncü bir yol hakkında herhangi bir öneriye açığım.

Yöntem 1:

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

Yöntem 2:

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

55
İkinci olan. Doğru bir fonksiyonun herhangi bir yan etkisi olmamalıdır, ilk uygulamanızda dış dünyayı değiştiriyorsunuzdur.
ThanksForAllTheFish

37
sadece bir stil meselesi, ancak elt -> elt != nulldeğiştirilebilirObjects::nonNull
the8472

2
@ the8472 Daha da iyisi, koleksiyonda null değerlerin bulunmadığından emin olmak ve Optional<T>bunun yerine ile kullanmaktır flatMap.
Herman

2
@SzymonRoziewski, pek değil. Bu kadar önemsiz bir şey için, kaputun altındaki paralel akışı kurmak için gereken iş, bu yapı sessizliğini kullanacaktır.
MK

2
Statik olmayan bir yöntem .map(this::doSomething)olduğunu varsayarak yazabileceğinizi unutmayın doSomething. Statik thisise, sınıf adıyla değiştirebilirsiniz .
Herman

Yanıtlar:


153

Herhangi bir performans farkı için endişelenmeyin, normalde bu durumda minimum olacaklardır.

Yöntem 2 tercih edilir çünkü

  1. lambda ifadesinin dışında var olan bir koleksiyonun mutasyona uğramasını gerektirmez,

  2. toplama kanalında gerçekleştirilen farklı adımlar sırasıyla yazılır: önce bir filtre işlemi, sonra bir harita işlemi, ardından sonucu toplama (toplama boru hatlarının faydaları hakkında daha fazla bilgi için Martin Fowler'in mükemmel makalesine bakın ),

  3. kullanılan değerleri değiştirerek değerlerin toplanma şeklini kolayca değiştirebilirsiniz Collector. Bazı durumlarda kendiniz yazmanız gerekebilir Collector, ancak faydası bunu kolayca yeniden kullanabilmenizdir.


43

Mevcut cevaplara, ikinci formun daha iyi olduğu konusunda katılıyorum, çünkü herhangi bir yan etkisi yoktur ve paralelleştirilmesi daha kolaydır (sadece paralel bir akış kullanın).

Performans açısından, paralel akışları kullanmaya başlayana kadar eşdeğer görünürler. Bu durumda, harita gerçekten çok daha iyi performans gösterecektir. Mikro karşılaştırmalı değerlendirme sonuçlarının altına bakın :

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

İlk örneği aynı şekilde artıramazsınız çünkü forEach bir terminal yöntemidir - geçersiz döner - böylece durum bilgisi olan bir lambda kullanmak zorunda kalırsınız. Ancak paralel akışlar kullanıyorsanız bu gerçekten kötü bir fikirdir .

Son olarak, ikinci snippet'inizin yöntem başvuruları ve statik içe aktarmalarla çok daha kısa bir şekilde yazılabileceğini unutmayın:

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

1
Performans hakkında, paralelStreams kullanırsanız, durumunuz "harita" gerçekten "forEach" kazanır. Milisaniye cinsinden tezgahlarım: SO28319064.forHer biri: 187,310 ± 1,768 ms / op - SO28319064.map: 189,180 ± 1,692 ms / op - SO28319064.map Paralel Akış: 55,577 ± 0,782 ms / op
Giuseppe Bertone

2
@GiuseppeBertone, asiliye bağlı, ama bence düzenlemeniz orijinal yazarın niyetiyle çelişiyor. Kendi cevabınızı eklemek istiyorsanız, mevcut cevabı düzenlemek yerine eklemek daha iyidir. Ayrıca şimdi mikrobençmarkın bağlantısı sonuçlar ile ilgili değildir.
Tagir Valeev

5

Akışları kullanmanın temel faydalarından biri, verileri bildirimsel bir şekilde, yani işlevsel bir programlama stili kullanarak işleme yeteneği kazandırmasıdır. Ayrıca, akışınızı eşzamanlı hale getirmek için ekstra çok iş parçacıklı kod yazmanıza gerek kalmadan, ücretsiz olarak çoklu iş parçacığı oluşturma yeteneği sağlar.

Bu programlama tarzını keşfetme nedeninizin bu avantajlardan yararlanmak istediğinizi varsayarsak, ilk kod örneğiniz potansiyel olarak işlevsel değildir. foreach yöntem terminal olarak sınıflandırıldığı için (yani yan etkiler üretebileceği .

İkinci yol fonksiyonel programlama açısından tercih edilir, çünkü harita fonksiyonu durumsuz lambda fonksiyonlarını kabul edebilir. Daha açık bir şekilde, harita işlevine geçirilen lambda,

  1. Parazit yapmaz, yani işlev eşzamanlı değilse (örneğin ArrayList) akış kaynağını değiştirmemelidir .
  2. Paralel işleme (iş parçacığı zamanlama farklılıkları nedeniyle) yaparken beklenmedik sonuçlardan kaçınmak için vatansız.

İkinci yaklaşımın bir başka yararı da akışın paralel olması ve toplayıcının eşzamanlı ve sırasız olması, bu özelliklerin toplanması aynı anda yapmak için azaltma işlemine faydalı ipuçları sağlayabilir.


4

Eğer kullanırsanız Eclipse Koleksiyonları kullanabilirsiniz collectIf()yöntemi.

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

İstekli bir şekilde değerlendirir ve bir Akış kullanmaktan biraz daha hızlı olmalıdır.

Not: Eclipse Collections için bir komisyoncuyum.


1

İkinci yolu tercih ederim.

İlk yolu kullandığınızda, performansı artırmak için paralel bir akış kullanmaya karar verirseniz, öğelerin çıktı listesine eklenme sırası üzerinde hiçbir denetiminiz olmaz forEach.

Kullandığınızda toList, akışlar paralel bir akış kullansanız bile sırayı korur.


Bu doğru tavsiyem de emin değilim: o kullanabilirsiniz forEachOrderedyerine forEacho paralel akış kullanmak ama hala düzeni korumak istiyorsa. Fakat forEachdevletler için dokümantasyon olarak, karşılaşma düzenini korumak paralelliğin faydasını feda eder. O zaman da böyle olduğunu düşünüyorum toList.
Herman

0

Akışın neden bir toList yöntemine sahip olmadığınınstream().toArray() altındaki üçüncü bir seçenek - kullanma - yorumlarına bakın . ForEach () veya collect () 'den daha yavaş ve daha az etkileyici olduğu ortaya çıkıyor. Daha sonraki JDK derlemelerinde optimize edilebilir, bu yüzden her ihtimale karşı buraya ekleyin.

varsayarak List<String>

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

mikro-mikro karşılaştırmalı değerlendirme, 1M girişler,% 20 boş ve doSomething () içinde basit dönüşüm

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

sonuçlar

paralel:

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}

ardışık:

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}

nulls ve filtre olmadan paralel (böylece akış SIZED): toArrays bu durumda en iyi performansa sahiptir ve .forEach()alıcı ArrayList üzerindeki "indexOutOfBounds" ile başarısız olursa,.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

Yöntem 3 olabilir.

Her zaman mantığı ayrı tutmayı tercih ederim.

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

3. Pary Libaries kullanıyorsanız, cyclops -tepki , bu işlevselliği yerleşik tembel genişletilmiş koleksiyonları tanımlar. Örneğin,

ListX myListToParse;

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

myFinalList ilk erişime kadar değerlendirilmez (ve somutlaştırılmış liste önbelleğe alındıktan ve tekrar kullanıldıktan sonra).

[Açıklama Ben cyclops-tepki reaksiyonu geliştiricisiyim]

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.