Java 8 - Cara terbaik untuk mengubah daftar: peta atau foreach?

188

Saya memiliki daftar myListToParsetempat saya ingin memfilter elemen dan menerapkan metode pada setiap elemen, dan menambahkan hasilnya di daftar lain myFinalList.

Dengan Java 8 saya perhatikan bahwa saya dapat melakukannya dengan 2 cara berbeda. Saya ingin tahu cara yang lebih efisien di antara mereka dan memahami mengapa satu cara lebih baik daripada yang lain.

Saya terbuka untuk saran tentang cara ketiga.

Metode 1:

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

Metode 2:

myFinalList = myListToParse.stream()
        .filter(elt -> elt != null)
        .map(elt -> doSomething(elt))
        .collect(Collectors.toList()); 
Emilien Brigand
sumber
55
Yang kedua. Fungsi yang tepat seharusnya tidak memiliki efek samping, dalam implementasi pertama Anda, Anda memodifikasi dunia eksternal.
ThanksForAllTheFish
37
hanya masalah gaya, tetapi elt -> elt != nulldapat diganti denganObjects::nonNull
the8472
2
@ the8472 Bahkan lebih baik adalah memastikan tidak ada nilai null dalam koleksi di tempat pertama, dan gunakan Optional<T>sebaliknya dalam kombinasi dengan flatMap.
herman
2
@SzymonRoziewski, tidak cukup. Untuk sesuatu yang sepele seperti ini, pekerjaan yang diperlukan untuk mensetup paralelstream di bawah tenda akan membuat menggunakan konstruk bisu ini.
MK
2
Perhatikan bahwa Anda dapat menulis .map(this::doSomething)dengan asumsi itu doSomethingadalah metode yang tidak statis. Jika statis, Anda dapat mengganti thisdengan nama kelas.
herman

Jawaban:

153

Jangan khawatir tentang perbedaan kinerja, mereka akan menjadi minimal dalam hal ini secara normal.

Metode 2 lebih disukai karena

  1. itu tidak memerlukan mutasi koleksi yang ada di luar ekspresi lambda,

  2. itu lebih mudah dibaca karena langkah-langkah berbeda yang dilakukan dalam pipa koleksi ditulis secara berurutan: pertama operasi filter, kemudian operasi peta, kemudian mengumpulkan hasilnya (untuk info lebih lanjut tentang manfaat pipa koleksi, lihat artikel bagus Martin Fowler ),

  3. Anda dapat dengan mudah mengubah cara nilai dikumpulkan dengan mengganti Collectoryang digunakan. Dalam beberapa kasus Anda mungkin perlu menulis sendiri Collector, tetapi manfaatnya adalah Anda dapat dengan mudah menggunakannya kembali.

herman
sumber
43

Saya setuju dengan jawaban yang ada bahwa bentuk kedua lebih baik karena tidak memiliki efek samping dan lebih mudah disejajarkan (cukup gunakan aliran paralel).

Dari segi kinerja, tampaknya keduanya setara hingga Anda mulai menggunakan aliran paralel. Dalam hal ini, peta akan tampil jauh lebih baik. Lihat di bawah hasil patokan 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

Anda tidak dapat meningkatkan contoh pertama dengan cara yang sama karena forEach adalah metode terminal - ia mengembalikan batal - jadi Anda terpaksa menggunakan lambda stateful. Tapi itu benar-benar ide yang buruk jika Anda menggunakan aliran paralel .

Akhirnya perhatikan bahwa cuplikan kedua Anda dapat ditulis dengan cara yang lebih ringkas dengan referensi metode dan impor statis:

myFinalList = myListToParse.stream()
    .filter(Objects::nonNull)
    .map(this::doSomething)
    .collect(toList()); 
assylias
sumber
1
Tentang kinerja, dalam kasus Anda "peta" benar-benar menang atas "forEach" jika Anda menggunakan parallelStreams. Bangku saya dalam milidetik: SO28319064.forSetiap: 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, terserah assylias, tetapi menurut pendapat saya suntingan Anda bertentangan dengan maksud penulis asli. Jika Anda ingin menambahkan jawaban Anda sendiri, lebih baik menambahkannya daripada mengedit yang sudah ada begitu banyak. Juga sekarang tautan ke microbenchmark tidak relevan dengan hasil.
Tagir Valeev
5

Salah satu manfaat utama menggunakan stream adalah memberikan aliran untuk memproses data dengan cara deklaratif, yaitu menggunakan gaya pemrograman fungsional. Ini juga memberikan kemampuan multi-threading untuk makna gratis, tidak perlu menulis kode multi-utas tambahan untuk membuat aliran Anda bersamaan.

Dengan asumsi alasan Anda menjelajahi gaya pemrograman ini adalah bahwa Anda ingin mengeksploitasi manfaat ini maka sampel kode pertama Anda berpotensi tidak berfungsi karena foreachmetode ini digolongkan sebagai terminal (artinya dapat menghasilkan efek samping).

Cara kedua lebih disukai dari sudut pandang pemrograman fungsional karena fungsi peta dapat menerima fungsi lambda stateless. Lebih eksplisit lagi, lambda yang diteruskan ke fungsi peta seharusnya

  1. Non-interferensi, artinya fungsi tidak boleh mengubah sumber aliran jika tidak bersamaan (misalnya ArrayList).
  2. Stateless untuk menghindari hasil yang tidak terduga ketika melakukan pemrosesan paralel (disebabkan oleh perbedaan penjadwalan thread).

Manfaat lain dengan pendekatan kedua adalah jika aliran paralel dan kolektor bersamaan dan tidak berurutan maka karakteristik ini dapat memberikan petunjuk yang berguna untuk operasi pengurangan untuk melakukan pengumpulan secara bersamaan.

MK
sumber
4

Jika Anda menggunakan Eclipse Collections, Anda dapat menggunakan collectIf()metode ini.

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

Ini mengevaluasi dengan bersemangat dan harus sedikit lebih cepat daripada menggunakan Stream.

Catatan: Saya pengendara untuk Eclipse Collections.

Craig P. Motlin
sumber
1

Saya lebih suka cara kedua.

Ketika Anda menggunakan cara pertama, jika Anda memutuskan untuk menggunakan aliran paralel untuk meningkatkan kinerja, Anda tidak akan memiliki kontrol atas urutan di mana elemen akan ditambahkan ke daftar output oleh forEach.

Saat Anda menggunakan toList, Streams API akan mempertahankan pesanan meskipun Anda menggunakan aliran paralel.

Eran
sumber
Saya tidak yakin ini saran yang benar: dia bisa menggunakan forEachOrderedalih-alih forEachjika dia ingin menggunakan aliran paralel tetapi tetap mempertahankan pesanan. Tetapi sebagai dokumentasi untuk forEachnegara, menjaga tatanan perjumpaan mengorbankan manfaat paralelisme. Saya menduga itu juga halnya dengan toListitu.
herman
0

Ada opsi ketiga - menggunakan stream().toArray()- lihat komentar di bawah mengapa streaming tidak memiliki metode toList . Ternyata lebih lambat daripada forEach () atau collect (), dan kurang ekspresif. Mungkin dioptimalkan di JDK build selanjutnya, jadi menambahkannya di sini untuk berjaga-jaga.

asumsi List<String>

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

dengan tolok ukur mikro-mikro, entri 1M, 20% nol, dan transformasi sederhana di 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;
}

hasilnya

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}

sekuensial:

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}

sejajar tanpa nulls dan filter (demikian juga arusnya SIZED): toArrays memiliki kinerja terbaik dalam kasus seperti itu, dan .forEach()gagal dengan "indexOutOfBounds" pada ArrayList penerima, harus diganti dengan.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
sumber
0

Mungkin Metode 3.

Saya selalu lebih suka memisahkan logika.

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
sumber
0

Jika menggunakan 3rd Pary Libaries ok cyclops-react mendefinisikan koleksi Lazy yang diperluas dengan fungsi ini. Misalnya, kita cukup menulis

ListX myListToParse;

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

myFinalList tidak dievaluasi sampai akses pertama (dan setelah daftar terwujud di-cache dan digunakan kembali).

[Pengungkapan Saya adalah pengembang utama cyclops-react]

John McClean
sumber