Operasi aliran menengah tidak dievaluasi berdasarkan hitungan

33

Sepertinya saya mengalami kesulitan memahami bagaimana Jawa menyusun operasi aliran ke dalam aliran pipa.

Saat menjalankan kode berikut

public
 static void main(String[] args) {
    StringBuilder sb = new StringBuilder();

    var count = Stream.of(new String[]{"1", "2", "3", "4"})
            .map(sb::append)
            .count();

    System.out.println(count);
    System.out.println(sb.toString());
}

Konsol hanya mencetak 4. The StringBuilderobjek masih memiliki nilai "".

Ketika saya menambahkan operasi filter: filter(s -> true)

public static void main(String[] args) {
    StringBuilder sb = new StringBuilder();

    var count = Stream.of(new String[]{"1", "2", "3", "4"})
            .filter(s -> true)
            .map(sb::append)
            .count();

    System.out.println(count);
    System.out.println(sb.toString());
}

Output berubah menjadi:

4
1234

Bagaimana operasi filter yang tampaknya berlebihan ini mengubah perilaku pipa aliran yang dibuat?

atalantus
sumber
2
Menarik !!!
uneq95
3
Saya akan membayangkan ini adalah perilaku implementasi khusus; mungkin itu karena aliran pertama memiliki ukuran yang diketahui, tetapi yang kedua tidak, dan ukuran menentukan apakah operasi perantara dijalankan.
Andy Turner
Tidak tertarik, apa yang terjadi jika Anda membalikkan filter dan peta?
Andy Turner
Setelah diprogram sedikit di Haskell, baunya seperti evaluasi malas yang terjadi di sini. Pencarian google kembali, aliran itu memang memiliki kemalasan. Mungkinkah itu masalahnya? Dan tanpa filter, jika java cukup pintar, tidak perlu melakukan pemetaan.
Frederik
@AndyTurner Ini memberikan hasil yang sama, bahkan pada pembalikan
uneq95

Jawaban:

39

The count()operasi terminal, dalam versi saya dari JDK, akhirnya mengeksekusi kode berikut:

if (StreamOpFlag.SIZED.isKnown(helper.getStreamAndOpFlags()))
    return spliterator.getExactSizeIfKnown();
return super.evaluateSequential(helper, spliterator);

Jika ada filter()operasi dalam pipa operasi, ukuran aliran, yang diketahui pada awalnya, tidak dapat diketahui lagi (karena filterbisa menolak beberapa elemen aliran). Jadi ifblok tidak dieksekusi, operasi menengah dieksekusi dan StringBuilder diubah.

Di sisi lain, Jika Anda hanya memiliki map()dalam pipa, jumlah elemen dalam aliran dijamin sama dengan jumlah elemen awal. Jadi jika blok dijalankan, dan ukuran dikembalikan secara langsung tanpa mengevaluasi operasi perantara.

Perhatikan bahwa lambda map()dianggap melanggar kontrak yang didefinisikan dalam dokumentasi: itu seharusnya merupakan operasi tanpa kewarganegaraan yang tanpa campur tangan, tetapi itu bukan stateless. Jadi memiliki hasil yang berbeda dalam kedua kasus tidak dapat dianggap sebagai bug.

JB Nizet
sumber
Karena flatMap()mungkin bisa mengubah jumlah elemen, apakah itu alasan mengapa awalnya bersemangat (sekarang malas)? Jadi, alternatifnya adalah menggunakan forEach()dan menghitung secara terpisah jika map()dalam bentuk saat ini melanggar kontrak, saya kira.
Frederik
3
Mengenai flatMap, saya rasa tidak. Itu, AFAIK, karena awalnya lebih sederhana untuk membuatnya bersemangat. Ya, menggunakan stream, dengan map (), untuk menghasilkan efek samping adalah ide yang buruk.
JB Nizet
Apakah Anda memiliki saran tentang cara mencapai hasil penuh 4 1234tanpa menggunakan filter tambahan atau menghasilkan efek samping dalam operasi map ()?
atalantus
1
int count = array.length; String result = String.join("", array);
JB Nizet
1
atau Anda bisa menggunakan forEach jika Anda benar-benar ingin menggunakan StringBuilder, atau Anda bisa menggunakanCollectors.joining("")
njzk2
19

Dalam jdk-9 itu jelas didokumentasikan dalam java docs

Penghapusan efek samping juga mungkin mengejutkan. Dengan pengecualian operasi terminal untuk setiap orang dan untuk setiap orang, efek samping dari parameter perilaku mungkin tidak selalu dieksekusi ketika implementasi stream dapat mengoptimalkan jauh eksekusi parameter perilaku tanpa mempengaruhi hasil perhitungan. (Untuk contoh spesifik, lihat catatan API yang didokumentasikan pada operasi penghitungan .)

Catatan API:

Suatu implementasi dapat memilih untuk tidak mengeksekusi pipa aliran (baik secara berurutan atau paralel) jika mampu menghitung secara langsung dari sumber aliran. Dalam kasus seperti itu, tidak ada elemen sumber yang akan dilalui dan tidak ada operasi perantara yang akan dievaluasi. Parameter perilaku dengan efek samping, yang sangat tidak dianjurkan kecuali untuk kasus yang tidak berbahaya seperti debugging, dapat terpengaruh. Misalnya, pertimbangkan aliran berikut:

 List<String> l = Arrays.asList("A", "B", "C", "D");
 long count = l.stream().peek(System.out::println).count();

Jumlah elemen yang dicakup oleh sumber aliran, Daftar, diketahui dan operasi perantara, mengintip, tidak menyuntikkan ke dalam atau menghapus elemen dari aliran (seperti halnya untuk operasi flatMap atau filter). Jadi hitungannya adalah ukuran Daftar dan tidak perlu mengeksekusi pipa dan, sebagai efek samping, mencetak elemen daftar.

Kolam kematian
sumber
0

Ini bukan untuk apa .map. Seharusnya digunakan untuk mengubah aliran "Sesuatu" menjadi aliran "Sesuatu Lain". Dalam hal ini, Anda menggunakan peta untuk menambahkan string ke Stringbuilder eksternal, setelah itu Anda memiliki aliran "Stringbuilder", yang masing-masing dibuat oleh operasi peta menambahkan satu nomor ke Stringbuilder asli.

Aliran Anda tidak benar-benar melakukan apa pun dengan hasil yang dipetakan di arus, sehingga sangat masuk akal untuk menganggap bahwa langkah tersebut dapat dilewati oleh prosesor aliran. Anda mengandalkan efek samping untuk melakukan pekerjaan, yang merusak model fungsional peta. Anda akan lebih baik dilayani dengan menggunakan forEach untuk melakukan ini. Lakukan penghitungan sebagai aliran terpisah sepenuhnya, atau letakkan penghitung menggunakan AtomicInt di forEach.

Filter memaksanya untuk menjalankan konten stream karena sekarang harus melakukan sesuatu yang secara bermakna bermakna dengan setiap elemen stream.

DaveB
sumber