Tidak seperti C # IEnumerable
, di mana pipa eksekusi dapat dieksekusi sebanyak yang kita inginkan, di Jawa stream dapat 'diulang' hanya sekali.
Setiap panggilan ke operasi terminal menutup aliran, menjadikannya tidak dapat digunakan. 'Fitur' ini menghilangkan banyak daya.
Saya membayangkan alasan untuk ini bukan teknis. Apa pertimbangan desain di balik pembatasan aneh ini?
Sunting: untuk menunjukkan apa yang saya bicarakan, pertimbangkan implementasi Quick-Sort di C #:
IEnumerable<int> QuickSort(IEnumerable<int> ints)
{
if (!ints.Any()) {
return Enumerable.Empty<int>();
}
int pivot = ints.First();
IEnumerable<int> lt = ints.Where(i => i < pivot);
IEnumerable<int> gt = ints.Where(i => i > pivot);
return QuickSort(lt).Concat(new int[] { pivot }).Concat(QuickSort(gt));
}
Sekarang untuk memastikan, saya tidak menganjurkan bahwa ini adalah implementasi cepat yang baik! Namun itu adalah contoh yang bagus dari kekuatan ekspresif ekspresi lambda dikombinasikan dengan operasi aliran.
Dan itu tidak bisa dilakukan di Jawa! Saya bahkan tidak dapat menanyakan aliran apakah itu kosong tanpa menjadikannya tidak dapat digunakan.
sumber
IEnumerable
dengan aliranjava.io.*
Jawaban:
Saya memiliki beberapa ingatan dari desain awal API Streams yang mungkin menjelaskan pemikiran desain.
Kembali pada tahun 2012, kami menambahkan lambdas ke bahasa tersebut, dan kami menginginkan serangkaian operasi yang berorientasi koleksi atau "data massal", diprogram menggunakan lambdas, yang akan memfasilitasi paralelisme. Gagasan operasi rantai malas bersama-sama sudah mapan pada titik ini. Kami juga tidak ingin operasi perantara menyimpan hasil.
Masalah utama yang perlu kami putuskan adalah seperti apa objek dalam rantai itu di API dan bagaimana mereka terhubung ke sumber data. Sumber sering koleksi, tetapi kami juga ingin mendukung data yang berasal dari file atau jaringan, atau data yang dihasilkan saat itu juga, misalnya, dari generator angka acak.
Ada banyak pengaruh pekerjaan yang ada pada desain. Di antara yang lebih berpengaruh adalah perpustakaan Google Guava dan perpustakaan koleksi Scala. (Jika ada yang terkejut tentang pengaruh dari Guava, perhatikan bahwa Kevin Bourrillion , pengembang utama Guava, berada di kelompok ahli JSR-335 Lambda .) Pada koleksi Scala, kami menemukan pembicaraan oleh Martin Odersky ini menjadi minat khusus: Masa Depan- Proofing Scala Collections: dari Mutable ke Persistent hingga Parallel . (Stanford EE380, 2011 Juni).
Desain prototipe kami pada saat itu berbasis di sekitar
Iterable
. Operasi familiarfilter
,map
dan sebagainya adalah ekstensi (default) metode padaIterable
. Memanggil satu menambahkan operasi ke rantai dan mengembalikan yang lainIterable
. Operasi terminal seperticount
akan memanggiliterator()
rantai ke sumber, dan operasi dilaksanakan dalam Iterator setiap tahap.Karena ini adalah Iterables, Anda dapat memanggil
iterator()
metode lebih dari sekali. Lalu apa yang harus terjadi?Jika sumbernya adalah koleksi, ini sebagian besar berfungsi dengan baik. Koleksi-koleksi adalah Iterable, dan setiap panggilan untuk
iterator()
menghasilkan instance Iterator yang berbeda yang tidak tergantung pada instance aktif lainnya, dan masing-masing melintasi koleksi secara independen. Bagus.Sekarang bagaimana jika sumbernya adalah sekali pakai, seperti membaca baris dari suatu file? Mungkin Iterator pertama harus mendapatkan semua nilai tetapi yang kedua dan selanjutnya harus kosong. Mungkin nilai-nilai harus disisipkan di antara para Iterator. Atau mungkin setiap Iterator harus mendapatkan semua nilai yang sama. Lalu, bagaimana jika Anda memiliki dua iterator dan satu semakin jauh di depan yang lain? Seseorang harus menyangga nilai-nilai di Iterator kedua sampai mereka membaca. Lebih buruk lagi, bagaimana jika Anda mendapatkan satu Iterator dan membaca semua nilai, dan hanya kemudian mendapatkan Iterator kedua. Dari mana nilai-nilai itu berasal sekarang? Apakah ada persyaratan bagi mereka semua untuk disangga kalau-kalau ada yang menginginkan Iterator kedua?
Jelas, memungkinkan beberapa Iterator atas sumber sekali pakai menimbulkan banyak pertanyaan. Kami tidak memiliki jawaban yang baik untuk mereka. Kami menginginkan perilaku yang konsisten dan dapat diprediksi untuk apa yang terjadi jika Anda menelepon
iterator()
dua kali. Ini mendorong kami untuk melarang beberapa jalur, membuat jalur pipa satu arah.Kami juga mengamati orang lain menabrak masalah ini. Di JDK, sebagian Iterables adalah koleksi atau objek seperti koleksi, yang memungkinkan banyak traversal. Itu tidak ditentukan di mana pun, tetapi tampaknya ada harapan tidak tertulis bahwa Iterables mengizinkan beberapa traversal. Pengecualian penting adalah antarmuka NIO DirectoryStream . Spesifikasinya mencakup peringatan yang menarik ini:
[tebal aslinya]
Ini tampak tidak biasa dan cukup tidak menyenangkan sehingga kami tidak ingin membuat sejumlah Iterables baru yang mungkin hanya sekali saja. Ini mendorong kami untuk menggunakan Iterable.
Tentang saat ini, sebuah artikel oleh Bruce Eckel muncul yang menggambarkan tempat masalah yang dia alami dengan Scala. Dia menulis kode ini:
Cukup mudah. Ini mem-parsing baris teks menjadi
Registrant
objek dan mencetaknya dua kali. Kecuali bahwa itu sebenarnya hanya mencetaknya sekali. Ternyata dia mengiraregistrants
itu koleksi, padahal sebenarnya itu iterator. Panggilan kedua untukforeach
menemukan iterator kosong, dari mana semua nilai telah habis, sehingga tidak mencetak apa pun.Pengalaman semacam ini meyakinkan kami bahwa sangat penting untuk memiliki hasil yang dapat diprediksi secara jelas jika dicoba beberapa kali traversal. Ini juga menyoroti pentingnya membedakan antara struktur seperti pipa yang malas dari koleksi aktual yang menyimpan data. Ini pada gilirannya mendorong pemisahan operasi pipa malas ke antarmuka Stream baru dan hanya menjaga operasi mutatif yang penuh semangat langsung pada Koleksi. Brian Goetz telah menjelaskan alasannya.
Bagaimana dengan memungkinkan beberapa traversal untuk jaringan pipa berbasis pengumpulan tetapi melarangnya untuk jaringan pipa non-koleksi? Ini tidak konsisten, tetapi masuk akal. Jika Anda membaca nilai dari jaringan, tentu saja Anda tidak dapat melintasinya lagi. Jika Anda ingin melintasi mereka beberapa kali, Anda harus menariknya ke dalam koleksi secara eksplisit.
Tapi mari kita jelajahi untuk memungkinkan beberapa traversal dari jaringan pipa berbasis koleksi. Katakanlah Anda melakukan ini:
(
into
Operasi sekarang diejacollect(toList())
.)Jika sumber adalah koleksi, maka
into()
panggilan pertama akan membuat rantai Iterator kembali ke sumber, menjalankan operasi pipa, dan mengirim hasilnya ke tujuan. Panggilan kedua untukinto()
akan membuat rantai Iterator lain, dan menjalankan operasi pipa lagi . Ini jelas tidak salah, tetapi memang memiliki efek melakukan semua operasi filter dan pemetaan untuk kedua elemen. Saya pikir banyak programmer akan terkejut dengan perilaku ini.Seperti yang saya sebutkan di atas, kami telah berbicara dengan pengembang Guava. Salah satu hal keren yang mereka miliki adalah Makam Ide di mana mereka menggambarkan fitur yang mereka memutuskan untuk tidak menerapkan bersama dengan alasannya. Gagasan koleksi malas terdengar sangat keren, tapi inilah yang mereka katakan tentang itu. Pertimbangkan
List.filter()
operasi yang mengembalikanList
:Untuk mengambil contoh spesifik, berapa biayanya
get(0)
atausize()
pada Daftar? Untuk kelas yang umum digunakan sepertiArrayList
, mereka O (1). Tetapi jika Anda memanggil salah satu dari ini pada daftar yang difilter dengan malas, ia harus menjalankan filter di atas daftar dukungan, dan tiba-tiba semua operasi ini adalah O (n). Lebih buruk lagi, harus melintasi daftar dukungan pada setiap operasi.Bagi kami ini sepertinya terlalu banyak kemalasan. Ini adalah satu hal untuk mengatur beberapa operasi dan menunda eksekusi yang sebenarnya sampai Anda jadi "Go". Merupakan hal lain untuk mengatur hal-hal sedemikian rupa sehingga menyembunyikan sejumlah besar potensi perhitungan ulang.
Dalam mengusulkan untuk melarang aliran yang tidak linier atau "tidak dapat digunakan kembali", Paul Sandoz menggambarkan konsekuensi potensial yang memungkinkan mereka menimbulkan "hasil yang tidak terduga atau membingungkan." Dia juga menyebutkan bahwa eksekusi paralel akan membuat segalanya lebih rumit. Akhirnya, saya akan menambahkan bahwa operasi pipa dengan efek samping akan menyebabkan bug yang sulit dan tidak jelas jika operasi tersebut dieksekusi secara tak terduga beberapa kali, atau setidaknya beberapa kali berbeda dari yang diharapkan oleh programmer. (Tapi programmer Java tidak menulis ekspresi lambda dengan efek samping, bukan? LAKUKAN MEREKA ??)
Jadi itulah dasar pemikiran untuk desain Java 8 Streams API yang memungkinkan one-shot traversal dan yang membutuhkan pipa yang benar-benar linier (tanpa bercabang). Ini memberikan perilaku yang konsisten di berbagai sumber aliran yang berbeda, itu jelas memisahkan operasi malas dari bersemangat, dan menyediakan model eksekusi langsung.
Berkenaan dengan
IEnumerable
, saya jauh dari ahli tentang C # dan .NET, jadi saya akan sangat menghargai dikoreksi (dengan lembut) jika saya menarik kesimpulan yang salah. Tampaknya, bagaimanapun, yangIEnumerable
memungkinkan beberapa traversal untuk berperilaku berbeda dengan sumber yang berbeda; dan itu memungkinkan struktur percabanganIEnumerable
operasi bersarang , yang dapat mengakibatkan beberapa perhitungan ulang yang signifikan. Sementara saya menghargai bahwa sistem yang berbeda menghasilkan pengorbanan yang berbeda, ini adalah dua karakteristik yang kami coba hindari dalam desain Java 8 Streams API.Contoh quicksort yang diberikan oleh OP menarik, membingungkan, dan saya minta maaf untuk mengatakan, agak mengerikan. Panggilan
QuickSort
membutuhkanIEnumerable
dan mengembalikanIEnumerable
, jadi tidak ada penyortiran yang benar-benar dilakukan hingga finalIEnumerable
dilalui. Apa yang tampaknya dilakukan oleh panggilan itu, adalah membangun struktur pohonIEnumerables
yang mencerminkan partisi yang akan dilakukan quicksort, tanpa benar-benar melakukannya. (Bagaimanapun, ini adalah perhitungan malas.) Jika sumber memiliki elemen N, pohon akan menjadi elemen N lebar di terluas, dan itu akan menjadi level lg (N).Bagi saya - dan sekali lagi, saya bukan pakar C # atau .NET - bahwa ini akan menyebabkan panggilan tertentu yang tampak tidak berbahaya, seperti pemilihan pivot via
ints.First()
, menjadi lebih mahal daripada yang terlihat. Pada level pertama, tentu saja, itu O (1). Tetapi pertimbangkan sebuah partisi jauh di dalam pohon, di tepi kanan. Untuk menghitung elemen pertama dari partisi ini, seluruh sumber harus dilalui, operasi O (N). Tetapi karena partisi di atas malas, mereka harus dihitung ulang, membutuhkan perbandingan O (lg N). Jadi memilih pivot akan menjadi operasi O (N lg N), yang semahal seluruh jenis.Tapi kami tidak benar-benar menyortir sampai kami melintasi yang kembali
IEnumerable
. Dalam algoritma quicksort standar, setiap level partisi menggandakan jumlah partisi. Setiap partisi hanya setengah ukuran, sehingga setiap level tetap pada kompleksitas O (N). Pohon partisi adalah O (lg N) tinggi, sehingga total pekerjaan adalah O (N lg N).Dengan pohon malas IEnumerables, di bagian bawah pohon ada N partisi. Komputasi setiap partisi membutuhkan lintasan elemen N, yang masing-masing membutuhkan perbandingan lg (N) di atas pohon. Untuk menghitung semua partisi di bagian bawah pohon, maka, membutuhkan perbandingan O (N ^ 2 lg N).
(Apakah ini benar? Saya hampir tidak bisa mempercayainya. Seseorang tolong periksa ini untuk saya.)
Bagaimanapun, itu memang keren yang
IEnumerable
dapat digunakan dengan cara ini untuk membangun struktur komputasi yang rumit. Tetapi jika itu memang meningkatkan kompleksitas komputasi seperti yang saya kira, kompleksitas pemrograman seperti ini adalah sesuatu yang harus dihindari kecuali seseorang sangat berhati-hati.sumber
ints
: "Kemungkinan enumerasi ganda dari IEnumerable". Menggunakan hal yang samaIEenumerable
lebih dari satu kali itu mencurigakan dan harus dihindari. Saya juga menunjukkan pertanyaan ini (yang telah saya jawab), yang menunjukkan beberapa peringatan dengan pendekatan .Net (selain kinerja yang buruk): Daftar <T> dan perbedaan IEnumerableLatar Belakang
Meskipun pertanyaannya tampak sederhana, jawaban yang sebenarnya membutuhkan latar belakang yang masuk akal. Jika Anda ingin melewatkan kesimpulan, gulir ke bawah ...
Pilih titik perbandingan Anda - Fungsi dasar
Menggunakan konsep dasar, konsep C #
IEnumerable
lebih dekat hubungannya dengan JavaIterable
, yang mampu membuat sebanyak Iterator yang Anda inginkan.IEnumerables
membuatIEnumerators
.Iterable
Buat JawaIterators
Sejarah setiap konsep serupa, dalam hal keduanya
IEnumerable
danIterable
memiliki motivasi dasar untuk memungkinkan perulangan gaya 'untuk-masing-masing' atas anggota kumpulan data. Itu penyederhanaan berlebihan karena mereka berdua memungkinkan lebih dari itu, dan mereka juga tiba pada tahap itu melalui perkembangan yang berbeda, tetapi itu adalah fitur umum yang signifikan.Mari kita bandingkan fitur itu: dalam kedua bahasa, jika sebuah kelas mengimplementasikan
IEnumerable
/Iterable
, maka kelas itu harus mengimplementasikan setidaknya satu metode tunggal (untuk C #, iniGetEnumerator
dan untuk Java ituiterator()
). Dalam setiap kasus, instance yang dikembalikan dari yang (IEnumerator
/Iterator
) memungkinkan Anda untuk mengakses anggota data saat ini dan selanjutnya. Fitur ini digunakan dalam sintaks untuk-masing-masing bahasa.Pilih titik perbandingan Anda - Fungsionalitas yang ditingkatkan
IEnumerable
di C # telah diperluas untuk memungkinkan sejumlah fitur bahasa lainnya ( sebagian besar terkait dengan Linq ). Fitur yang ditambahkan termasuk pilihan, proyeksi, agregasi, dll. Ekstensi ini memiliki motivasi yang kuat dari penggunaan dalam teori-set, mirip dengan konsep SQL dan Database Relasional.Java 8 juga memiliki fungsionalitas yang ditambahkan untuk memungkinkan tingkat pemrograman fungsional menggunakan Streams dan Lambdas. Perhatikan bahwa aliran Java 8 tidak terutama dimotivasi oleh teori himpunan, tetapi oleh pemrograman fungsional. Bagaimanapun, ada banyak persamaan.
Jadi, ini adalah poin kedua. Perangkat tambahan yang dibuat untuk C # diimplementasikan sebagai perangkat tambahan untuk
IEnumerable
konsep. Namun di Jawa, peningkatan yang dilakukan diimplementasikan dengan menciptakan konsep dasar baru Lambdas dan Streams, dan kemudian juga menciptakan cara yang relatif sepele untuk mengkonversi dariIterators
danIterables
ke Streams, dan sebaliknya.Jadi, membandingkan IEnumerable dengan konsep Stream Java tidak lengkap. Anda perlu membandingkannya dengan gabungan Streams dan Collections API di Jawa.
Di Jawa, Streaming tidak sama dengan Iterables, atau Iterators
Streaming tidak dirancang untuk menyelesaikan masalah dengan cara yang sama seperti iterator:
Dengan
Iterator
, Anda mendapatkan nilai data, memprosesnya, dan kemudian mendapatkan nilai data lainnya.Dengan Streams, Anda menghubungkan rangkaian fungsi secara bersamaan, lalu Anda mengumpankan nilai input ke stream, dan mendapatkan nilai output dari urutan gabungan. Catatan, dalam istilah Java, setiap fungsi dienkapsulasi dalam satu
Stream
instance. Streams API memungkinkan Anda untuk menautkan urutanStream
instance dengan cara yang mengaitkan urutan ekspresi transformasi.Untuk menyelesaikan
Stream
konsep, Anda membutuhkan sumber data untuk memberi makan aliran, dan fungsi terminal yang mengkonsumsi aliran.Cara Anda memasukkan nilai ke aliran mungkin sebenarnya dari
Iterable
, tetapiStream
urutan itu sendiri bukanIterable
, itu adalah fungsi majemuk.A
Stream
juga dimaksudkan untuk menjadi malas, dalam arti bahwa itu hanya berfungsi ketika Anda meminta nilai darinya.Perhatikan asumsi dan fitur signifikan dari Streaming:
Stream
di Jawa adalah mesin transformasi, ia mengubah item data dalam satu negara, menjadi di negara lain.C # Perbandingan
Ketika Anda menganggap bahwa Java Stream hanya bagian dari sistem pasokan, aliran, dan pengumpulan, dan bahwa Streaming dan Iterator sering digunakan bersama dengan Koleksi, maka tidak mengherankan bahwa sulit untuk berhubungan dengan konsep yang sama yaitu hampir semua tertanam dalam
IEnumerable
konsep tunggal dalam C #.Bagian-bagian dari IEnumerable (dan konsep-konsep terkait erat) tampak jelas di semua konsep Java Iterator, Iterable, Lambda, dan Stream.
Ada hal-hal kecil yang dapat dilakukan konsep Java yang lebih sulit di IEnumerable, dan sebaliknya.
Kesimpulan
Menambahkan Streaming memberi Anda lebih banyak pilihan saat memecahkan masalah, yang adil untuk diklasifikasikan sebagai 'meningkatkan kekuatan', bukan 'mengurangi', 'mengambil', atau 'membatasi' itu.
Mengapa Java Streaming sekali saja?
Pertanyaan ini salah arah, karena stream adalah urutan fungsi, bukan data. Bergantung pada sumber data yang mengumpan aliran, Anda dapat mengatur ulang sumber data, dan mengumpan aliran yang sama, atau berbeda.
Tidak seperti C # 's IEnumerable, di mana sebuah pipeline eksekusi dapat dieksekusi sebanyak yang kita inginkan, di Java stream dapat' di-iterated 'hanya sekali.
Membandingkan suatu
IEnumerable
ke yangStream
salah arah. Konteks yang Anda gunakan untuk mengatakanIEnumerable
dapat dieksekusi sebanyak yang Anda inginkan, paling baik dibandingkan dengan JavaIterables
, yang dapat diulang sebanyak yang Anda inginkan. JavaStream
mewakili subset dariIEnumerable
konsep, dan bukan subset yang memasok data, dan dengan demikian tidak dapat 'dijalankan kembali'.Setiap panggilan ke operasi terminal menutup aliran, menjadikannya tidak dapat digunakan. 'Fitur' ini menghilangkan banyak daya.
Pernyataan pertama itu benar, dalam arti tertentu. Pernyataan 'mengambil kekuasaan' tidak. Anda masih membandingkan Streams it IEnumerables. Operasi terminal dalam aliran seperti klausa 'break' dalam for loop. Anda selalu bebas untuk memiliki aliran lain, jika Anda mau, dan jika Anda dapat menyediakan kembali data yang Anda butuhkan. Sekali lagi, jika Anda menganggapnya
IEnumerable
lebih sepertiIterable
, untuk pernyataan ini, Java tidak apa-apa.Saya membayangkan alasan untuk ini bukan teknis. Apa pertimbangan desain di balik pembatasan aneh ini?
Alasannya teknis, dan untuk alasan sederhana bahwa Stream merupakan bagian dari apa yang dipikirkannya. Subset aliran tidak mengontrol suplai data, jadi Anda harus mengatur ulang suplai, bukan aliran. Dalam konteks itu, tidak aneh.
Contoh QuickSort
Contoh quicksort Anda memiliki tanda tangan:
Anda memperlakukan input
IEnumerable
sebagai sumber data:Selain itu, nilai balik
IEnumerable
juga, yang merupakan suplai data, dan karena ini adalah operasi Sortir, urutan suplai itu signifikan. Jika Anda menganggapIterable
kelas Java sebagai pasangan yang cocok untuk ini, khususnyaList
spesialisasiIterable
, karena Daftar adalah pasokan data yang memiliki urutan atau pengulangan yang dijamin, maka kode Java yang setara dengan kode Anda adalah:Perhatikan ada bug (yang saya buat ulang), karena jenisnya tidak menangani nilai duplikat dengan anggun, itu adalah jenis 'nilai unik'.
Perhatikan juga bagaimana kode Java menggunakan sumber data (
List
), dan konsep aliran pada titik yang berbeda, dan bahwa dalam C # kedua 'kepribadian' dapat diekspresikan hanyaIEnumerable
. Juga, meskipun saya telah menggunakanList
sebagai tipe dasar, saya bisa menggunakan yang lebih umumCollection
, dan dengan konversi iterator-to-Stream yang kecil, saya bisa menggunakan yang lebih umum lagiIterable
sumber
Stream
adalah konsep point-in-time, bukan 'operasi loop' .... (lanjutan)f(x)
. Stream tersebut mengenkapsulasi fungsi, itu tidak merangkum data yang mengalir melaluiIEnumerable
juga dapat menyediakan nilai acak, tidak terikat, dan menjadi aktif sebelum data ada.IEnumerable<T>
untuk merepresentasikan koleksi terbatas yang dapat diulang beberapa kali. Beberapa hal yang dapat diubah tetapi tidak memenuhi persyaratan yang diterapkanIEnumerable<T>
karena tidak ada antarmuka standar yang sesuai dengan tagihan, tetapi metode yang mengharapkan koleksi terbatas yang dapat diulang berkali-kali cenderung mengalami kerusakan jika diberikan hal yang dapat diubah yang tidak mematuhi kondisi tersebut .quickSort
Contoh Anda bisa jauh lebih sederhana jika mengembalikanStream
; itu akan menghemat dua.stream()
panggilan dan satu.collect(Collectors.toList())
panggilan. Jika Anda kemudian menggantiCollections.singleton(pivot).stream()
denganStream.of(pivot)
kode menjadi hampir dapat dibaca ...Stream
S dibangun di sekitarSpliterator
s yang merupakan objek stateable, bisa berubah. Mereka tidak memiliki tindakan "reset" dan pada kenyataannya, yang diperlukan untuk mendukung tindakan mundur tersebut akan "mengambil banyak daya". BagaimanaRandom.ints()
seharusnya menangani permintaan seperti itu?Di sisi lain, untuk
Stream
s yang memiliki asal yang dapat dilacak, mudah untuk membuat persamaanStream
untuk digunakan lagi. Cukup letakkan langkah-langkah yang dibuat untuk membangunnyaStream
menjadi metode yang dapat digunakan kembali. Ingatlah bahwa mengulangi langkah-langkah ini bukanlah operasi yang mahal karena semua langkah ini adalah operasi yang malas; pekerjaan yang sebenarnya dimulai dengan operasi terminal dan tergantung pada operasi terminal yang sebenarnya sama sekali kode yang berbeda dapat dijalankan.Terserah Anda, penulis metode seperti itu, untuk menentukan apa yang memanggil metode dua kali menyiratkan: apakah itu mereproduksi urutan yang sama persis, seperti aliran yang dibuat untuk array atau koleksi yang tidak dimodifikasi, atau apakah itu menghasilkan aliran dengan semantik serupa tetapi elemen berbeda seperti aliran int acak atau aliran jalur input konsol, dll.
By the way, kebingungan menghindari, operasi terminal mengkonsumsi tersebut
Stream
yang berbeda dari penutupan yangStream
seperti memanggilclose()
di sungai tidak (yang diperlukan untuk aliran setelah sumber daya seperti, misalnya diproduksi oleh terkaitFiles.lines()
).Tampaknya banyak kebingungan berasal dari perbandingan yang salah
IEnumerable
denganStream
. SebuahIEnumerable
mewakili kemampuan untuk memberikan yang sebenarnyaIEnumerator
, jadi sepertiIterable
di Jawa. Sebaliknya, aStream
adalah jenis iterator dan sebanding denganIEnumerator
sehingga salah untuk mengklaim bahwa tipe data jenis ini dapat digunakan beberapa kali dalam .NET, dukungannyaIEnumerator.Reset
adalah opsional. Contoh-contoh yang dibahas di sini lebih menggunakan fakta bahwa aIEnumerable
dapat digunakan untuk mengambil yang baruIEnumerator
dan yang bekerja dengan JavaCollection
juga; Anda bisa mendapatkan yang baruStream
. Jika pengembang Java memutuskan untuk menambahkanStream
operasi , itu benar-benar sebanding dan bisa bekerja dengan cara yang sama.Iterable
secara langsung, dengan operasi menengah mengembalikan yang lainIterable
Namun, pengembang memutuskan untuk tidak melakukannya dan keputusan tersebut dibahas dalam pertanyaan ini . Poin terbesar adalah kebingungan tentang operasi Collection bersemangat dan operasi Stream malas. Dengan melihat .NET API, saya (ya, secara pribadi) menganggapnya benar. Meskipun terlihat masuk akal melihat
IEnumerable
sendirian, Koleksi tertentu akan memiliki banyak metode memanipulasi Koleksi secara langsung dan banyak metode mengembalikan malasIEnumerable
, sedangkan sifat tertentu dari metode tidak selalu dapat dikenali secara intuitif. Contoh terburuk yang saya temukan (dalam beberapa menit saya melihatnya) adalahList.Reverse()
siapa yang namanya cocok persis dengan nama yang diwarisi (apakah ini terminus yang tepat untuk metode penyuluhan?)Enumerable.Reverse()
Sambil memiliki perilaku yang sepenuhnya bertentangan.Tentu saja, ini adalah dua keputusan berbeda. Yang pertama untuk membuat
Stream
jenis berbeda dariIterable
/Collection
dan yang kedua untuk membuatStream
semacam iterator satu kali daripada jenis lain dari iterable. Tetapi keputusan ini dibuat bersama-sama dan mungkin memisahkan kedua keputusan ini tidak pernah dipertimbangkan. Itu tidak dibuat dengan sebanding dengan .NET dalam pikiran.Keputusan desain API yang sebenarnya adalah untuk menambahkan jenis iterator yang ditingkatkan, the
Spliterator
.Spliterator
s dapat disediakan oleh yang lamaIterable
(yang merupakan cara bagaimana ini dipasang) atau implementasi yang sepenuhnya baru. Kemudian,Stream
ditambahkan sebagai front-end level tinggi ke level yang agak rendahSpliterator
. Itu dia. Anda dapat mendiskusikan tentang apakah desain yang berbeda akan lebih baik, tetapi itu tidak produktif, itu tidak akan berubah, mengingat cara mereka dirancang sekarang.Ada aspek implementasi lain yang harus Anda pertimbangkan.
Stream
s bukan struktur data yang tidak berubah. Setiap operasi perantara dapat mengembalikanStream
instance baru yang mengenkapsulasi yang lama tetapi juga dapat memanipulasi instance miliknya sendiri dan mengembalikannya sendiri (yang tidak menghalangi melakukan keduanya bahkan untuk operasi yang sama). Contoh yang umum dikenal adalah operasi sepertiparallel
atauunordered
yang tidak menambahkan langkah lain tetapi memanipulasi seluruh pipa). Memiliki struktur data yang bisa berubah dan upaya untuk menggunakan kembali (atau bahkan lebih buruk, menggunakannya berulang kali pada waktu yang sama) tidak berfungsi dengan baik ...Untuk kelengkapan, berikut adalah contoh quicksort Anda yang diterjemahkan ke Java
Stream
API. Ini menunjukkan bahwa itu tidak benar-benar "mengambil banyak kekuatan".Dapat digunakan seperti
Anda bahkan dapat menulisnya dengan lebih ringkas
sumber
Stream
sedangkan pengaturan ulang sumberSpliterator
akan tersirat. Dan saya cukup yakin jika itu mungkin, ada pertanyaan pada SO seperti “Mengapa meneleponcount()
dua kali padaStream
memberikan hasil yang berbeda setiap kali”, dll ...Stream
sejauh ini berasal dari upaya untuk memecahkan masalah dengan memanggil operasi terminal beberapa kali (jelas, jika tidak Anda tidak melihat) yang menyebabkan solusi rusak secara diam-diam jikaStream
API mengizinkannya dengan hasil berbeda pada setiap evaluasi. Ini adalah contoh yang bagus .Saya pikir ada sedikit perbedaan di antara keduanya ketika Anda melihat cukup dekat.
Pada wajahnya, sebuah
IEnumerable
tampaknya menjadi sebuah konstruksi yang dapat digunakan kembali:Namun, kompiler sebenarnya melakukan sedikit pekerjaan untuk membantu kami; itu menghasilkan kode berikut:
Setiap kali Anda benar-benar akan mengulangi enumerable, kompiler membuat enumerator. Pencacah tidak dapat digunakan kembali; panggilan lebih lanjut ke
MoveNext
hanya akan mengembalikan false, dan tidak ada cara untuk mengatur ulang ke awal. Jika Anda ingin mengulangi angka-angka lagi, Anda harus membuat instance enumerator lainnya.Untuk lebih menggambarkan bahwa IEnumerable memiliki (dapat memiliki) 'fitur' yang sama dengan Java Stream, pertimbangkan enumerable yang sumber bilangannya bukan koleksi statis. Sebagai contoh, kita dapat membuat objek enumerable yang menghasilkan urutan 5 angka acak:
Sekarang kita memiliki kode yang sangat mirip dengan enumerable berbasis array sebelumnya, tetapi dengan iterasi kedua berakhir
numbers
:Kali kedua kita mengulanginya
numbers
kita akan mendapatkan urutan angka yang berbeda, yang tidak dapat digunakan kembali dalam arti yang sama. Atau, kami dapat menulisRandomNumberStream
untuk melemparkan pengecualian jika Anda mencoba untuk mengulanginya berulang kali, membuat enumerable benar-benar tidak dapat digunakan (seperti Java Stream).Juga, apa arti penyortiran cepat berbasis enumerable Anda saat diterapkan ke
RandomNumberStream
?Kesimpulan
Jadi, perbedaan terbesar adalah bahwa .NET memungkinkan Anda untuk menggunakan kembali
IEnumerable
dengan secara implisit membuat yang baruIEnumerator
di latar belakang setiap kali diperlukan untuk mengakses elemen dalam urutan.Perilaku implisit ini sering berguna (dan 'kuat' seperti yang Anda nyatakan), karena kita dapat berulang kali mengulangi koleksi.
Namun terkadang, perilaku tersirat ini justru bisa menimbulkan masalah. Jika sumber data Anda tidak statis, atau mahal untuk diakses (seperti database atau situs web), maka banyak asumsi tentang
IEnumerable
harus dibuang; penggunaan kembali tidak lurus ke depansumber
Dimungkinkan untuk melewati beberapa perlindungan "jalankan sekali" di Stream API; misalnya kita dapat menghindari
java.lang.IllegalStateException
pengecualian (dengan pesan "streaming telah dioperasikan atau ditutup") dengan merujuk dan menggunakan kembaliSpliterator
(alih-alihStream
secara langsung).Misalnya, kode ini akan berjalan tanpa membuang pengecualian:
Namun output akan terbatas pada
daripada mengulangi output dua kali. Ini karena
ArraySpliterator
digunakan sebagaiStream
sumber stateful dan menyimpan posisi saat ini. Ketika kami memutar ulang ini,Stream
kami mulai lagi di akhir.Kami memiliki sejumlah opsi untuk mengatasi tantangan ini:
Kita dapat menggunakan
Stream
metode pembuatan stateless sepertiStream#generate()
. Kami harus mengelola status secara eksternal dalam kode kami sendiri dan mengatur ulang antaraStream
"replay":Solusi lain (sedikit lebih baik tetapi tidak sempurna) untuk ini adalah dengan menulis sendiri
ArraySpliterator
(atauStream
sumber serupa ) yang mencakup beberapa kapasitas untuk mereset penghitung saat ini. Jika kami menggunakannya untuk menghasilkan,Stream
kami berpotensi memutar ulang mereka dengan sukses.Solusi terbaik untuk masalah ini (menurut saya) adalah membuat salinan baru dari setiap stateful
Spliterator
yang digunakan dalamStream
pipa ketika operator baru dipanggil padaStream
. Ini lebih kompleks dan terlibat untuk diterapkan, tetapi jika Anda tidak keberatan menggunakan perpustakaan pihak ketiga, cyclop-react memilikiStream
implementasi yang melakukan hal ini. (Pengungkapan: Saya adalah pengembang utama untuk proyek ini.)Ini akan dicetak
seperti yang diharapkan.
sumber