Di stream Java apakah mengintip benar-benar hanya untuk debugging?

137

Saya membaca tentang aliran Jawa dan menemukan hal-hal baru saat saya melanjutkan. Salah satu hal baru yang saya temukan adalah peek()fungsinya. Hampir semua yang saya baca di mengintip mengatakan itu harus digunakan untuk men-debug Streaming Anda.

Bagaimana jika saya memiliki Stream di mana setiap Akun memiliki nama pengguna, bidang kata sandi dan metode login () dan login ().

saya juga punya

Consumer<Account> login = account -> account.login();

dan

Predicate<Account> loggedIn = account -> account.loggedIn();

Mengapa ini begitu buruk?

List<Account> accounts; //assume it's been setup
List<Account> loggedInAccount = 
accounts.stream()
    .peek(login)
    .filter(loggedIn)
    .collect(Collectors.toList());

Sejauh yang saya tahu ini melakukan persis apa yang seharusnya dilakukan. Itu;

  • Mengambil daftar akun
  • Mencoba masuk ke setiap akun
  • Menyaring semua akun yang tidak masuk
  • Mengumpulkan akun yang login ke daftar baru

Apa kerugian melakukan sesuatu seperti ini? Ada alasan saya tidak melanjutkan? Terakhir, jika bukan solusi ini lalu apa?

Versi asli ini menggunakan metode .filter () sebagai berikut;

.filter(account -> {
        account.login();
        return account.loggedIn();
    })
Adam.J
sumber
38
Setiap kali saya menemukan diri saya membutuhkan lambda multi-line, saya memindahkan baris ke metode pribadi dan meneruskan referensi metode alih-alih lambda.
VGR
1
Apa maksudnya - Anda mencoba masuk semua akun dan memfilternya berdasarkan apakah mereka masuk (yang mungkin sepele)? Atau, apakah Anda ingin masuk, lalu memfilternya berdasarkan apakah mereka sudah masuk atau tidak? Saya meminta ini dalam urutan ini karena forEachmungkin operasi yang Anda inginkan sebagai lawan peek. Hanya karena ada di API tidak berarti itu tidak terbuka untuk penyalahgunaan (seperti Optional.of).
Makoto
8
Perhatikan juga bahwa kode Anda bisa saja .peek(Account::login)dan .filter(Account::loggedIn); tidak ada alasan untuk menulis Consumer and Predicate yang hanya memanggil metode lain seperti itu.
Joshua Taylor
2
Juga perhatikan bahwa API aliran secara eksplisit mencegah efek samping dalam parameter perilaku .
Didier L
6
Konsumen yang berguna selalu memiliki efek samping, tentu saja itu tidak dianjurkan. Ini sebenarnya disebutkan di bagian yang sama: “ Sejumlah kecil operasi aliran, seperti forEach()dan peek(), hanya dapat beroperasi melalui efek samping; ini harus digunakan dengan hati-hati. ” Komentar saya lebih untuk mengingatkan bahwa peekoperasi (yang dirancang untuk tujuan debugging) tidak boleh diganti dengan melakukan hal yang sama di dalam operasi lain seperti map()atau filter().
Didier L

Jawaban:

77

Pengambilan kunci dari ini:

Jangan menggunakan API dengan cara yang tidak disengaja, bahkan jika itu mencapai tujuan langsung Anda. Pendekatan itu mungkin pecah di masa depan, dan juga tidak jelas bagi pengelola masa depan.


Tidak ada salahnya memecah ini ke beberapa operasi, karena mereka operasi yang berbeda. Ada adalah salahnya menggunakan API dengan cara yang tidak jelas dan tidak diinginkan, yang mungkin memiliki konsekuensi jika perilaku tertentu dimodifikasi di masa depan versi Jawa.

Menggunakan forEachpada operasi ini akan membuat jelas kepada pengelola bahwa ada efek samping yang dimaksudkan pada setiap elemen accounts, dan bahwa Anda melakukan beberapa operasi yang dapat mengubahnya.

Ini juga lebih konvensional dalam arti bahwa peekini adalah operasi antara yang tidak beroperasi pada seluruh pengumpulan sampai operasi terminal berjalan, tetapi forEachmemang operasi terminal. Dengan cara ini, Anda dapat membuat argumen kuat di sekitar perilaku dan aliran kode Anda sebagai lawan mengajukan pertanyaan tentang apakah peekakan berperilaku sama seperti forEachdalam konteks ini.

accounts.forEach(a -> a.login());
List<Account> loggedInAccounts = accounts.stream()
                                         .filter(Account::loggedIn)
                                         .collect(Collectors.toList());
Makoto
sumber
3
Jika Anda melakukan login pada langkah preprocessing, Anda tidak perlu streaming sama sekali. Anda dapat tampil forEachtepat di pengumpulan sumber:accounts.forEach(a -> a.login());
Holger
1
@ Holger: Poin yang sangat baik. Saya sudah memasukkan itu ke dalam jawabannya.
Makoto
2
@ Adam.J: Benar, jawaban saya lebih fokus pada pertanyaan umum yang terkandung dalam judul Anda, yaitu apakah metode ini benar-benar hanya untuk debugging, dengan menjelaskan aspek-aspek metode itu. Jawaban ini lebih menyatu pada use case Anda yang sebenarnya dan bagaimana melakukannya. Jadi bisa dibilang, bersama-sama mereka memberikan gambaran lengkap. Pertama, alasan mengapa ini bukan penggunaan yang dimaksudkan, kedua kesimpulannya, bukan untuk tetap menggunakan yang tidak diinginkan dan apa yang harus dilakukan sebagai gantinya. Yang terakhir akan memiliki penggunaan yang lebih praktis untuk Anda.
Holger
2
Tentu saja, itu jauh lebih mudah jika login()metode mengembalikan booleannilai yang menunjukkan status keberhasilan ...
Holger
3
Itulah yang saya tuju. Jika login()mengembalikan a boolean, Anda dapat menggunakannya sebagai predikat yang merupakan solusi terbersih. Ini masih memiliki efek samping, tapi itu boleh saja asalkan tidak mengganggu, yaitu loginproses` dari seseorang Accounttidak memiliki pengaruh pada proses login` dari yang lain Account.
Holger
111

Hal penting yang harus Anda pahami adalah aliran didorong oleh operasi terminal . Operasi terminal menentukan apakah semua elemen harus diproses atau tidak sama sekali. Begitu collectjuga operasi yang memproses setiap item, sedangkan findAnymungkin berhenti memproses item setelah itu menemukan elemen yang cocok.

Dan count()mungkin tidak memproses elemen apa pun ketika dapat menentukan ukuran aliran tanpa memproses item. Karena ini adalah pengoptimalan yang tidak dibuat di Java 8, tetapi yang akan di Java 9, mungkin ada kejutan ketika Anda beralih ke Java 9 dan memiliki kode yang mengandalkan count()pemrosesan semua item. Ini juga terhubung ke detail lain yang bergantung pada implementasi, misalnya bahkan di Jawa 9, implementasi referensi tidak akan dapat memprediksi ukuran sumber stream tak terbatas dikombinasikan dengan limitsementara tidak ada batasan mendasar yang mencegah prediksi tersebut.

Karena peekmemungkinkan "melakukan tindakan yang disediakan pada setiap elemen karena elemen dikonsumsi dari aliran yang dihasilkan ", itu tidak mengamanatkan pemrosesan elemen tetapi akan melakukan tindakan tergantung pada apa yang dibutuhkan operasi terminal. Ini menyiratkan bahwa Anda harus menggunakannya dengan sangat hati-hati jika Anda memerlukan pemrosesan tertentu, misalnya ingin menerapkan tindakan pada semua elemen. Ini berfungsi jika operasi terminal dijamin untuk memproses semua item, tetapi meskipun demikian, Anda harus yakin bahwa bukan pengembang selanjutnya yang mengubah operasi terminal (atau Anda lupa aspek halusnya).

Lebih lanjut, meskipun stream menjamin untuk menjaga urutan pertemuan untuk kombinasi operasi tertentu bahkan untuk stream paralel, jaminan ini tidak berlaku untuk peek. Saat mengumpulkan ke dalam daftar, daftar yang dihasilkan akan memiliki urutan yang tepat untuk aliran paralel yang dipesan, tetapi peektindakan dapat dipanggil dalam urutan yang sewenang-wenang dan secara bersamaan.

Jadi hal paling berguna yang dapat Anda lakukan peekadalah mencari tahu apakah elemen stream telah diproses yang persis seperti yang dikatakan dokumentasi API:

Metode ini ada terutama untuk mendukung debugging, di mana Anda ingin melihat elemen saat mereka melewati titik tertentu dalam pipa

Holger
sumber
apakah akan ada masalah, di masa depan atau sekarang, dalam kasus penggunaan OP? Apakah kodenya selalu melakukan apa yang dia inginkan?
ZhongYu
9
@ bayou.io: sejauh yang saya bisa lihat, tidak ada masalah dalam bentuk yang tepat ini . Tetapi ketika saya mencoba menjelaskan, menggunakannya dengan cara ini menyiratkan Anda harus mengingat aspek ini, bahkan jika Anda kembali ke kode satu atau dua tahun kemudian untuk memasukkan «permintaan fitur 9876» ke dalam kode ...
Holger
1
"Tindakan mengintip dapat dipanggil dalam perintah sewenang-wenang dan bersamaan". Tidakkah pernyataan ini bertentangan dengan aturan mereka tentang bagaimana mengintip bekerja, misalnya "ketika elemen dikonsumsi"?
Jose Martinez
5
@ Jose Martinez: Dikatakan "karena elemen dikonsumsi dari aliran yang dihasilkan ", yang bukan tindakan terminal tetapi pemrosesan, meskipun bahkan tindakan terminal dapat mengkonsumsi elemen yang rusak selama hasil akhir konsisten. Tetapi saya juga berpikir, frasa catatan API, “ melihat elemen-elemen ketika mereka mengalir melewati titik tertentu dalam pipa ” melakukan pekerjaan yang lebih baik dalam menggambarkannya.
Holger
23

Mungkin aturan praktisnya adalah bahwa jika Anda menggunakan mengintip di luar skenario "debug", Anda hanya harus melakukannya jika Anda yakin dengan apa kondisi terminasi dan penyaringan menengah. Sebagai contoh:

return list.stream().map(foo->foo.getBar())
                    .peek(bar->bar.publish("HELLO"))
                    .collect(Collectors.toList());

tampaknya menjadi kasus yang valid di mana Anda inginkan, dalam satu operasi untuk mengubah semua Foos menjadi Bar dan memberi tahu mereka semua halo.

Tampak lebih efisien dan elegan daripada sesuatu seperti:

List<Bar> bars = list.stream().map(foo->foo.getBar()).collect(Collectors.toList());
bars.forEach(bar->bar.publish("HELLO"));
return bars;

dan Anda tidak mengulangi koleksi dua kali.

chimera8
sumber
4

Saya akan mengatakan bahwa peekmemberikan kemampuan untuk mendesentralisasi kode yang dapat mengubah objek streaming, atau memodifikasi keadaan global (berdasarkan pada mereka), alih-alih memasukkan semuanya ke dalam fungsi sederhana atau komposisional yang diteruskan ke metode terminal.

Sekarang pertanyaannya mungkin: haruskah kita mengubah objek streaming atau mengubah status global dari dalam fungsi dalam pemrograman java gaya fungsional ?

Jika jawaban untuk salah satu dari 2 pertanyaan di atas adalah ya (atau: dalam beberapa kasus ya) maka peek()sudah pasti tidak hanya untuk tujuan debugging , untuk alasan yang sama yang forEach()tidak hanya untuk tujuan debugging .

Bagi saya ketika memilih antara forEach()dan peek(), apakah memilih yang berikut: Apakah saya ingin potongan kode yang mengubah objek stream untuk dilampirkan ke komposer, atau apakah saya ingin mereka melampirkan langsung ke streaming?

Saya pikir peek()akan lebih baik memasangkan dengan metode java9. misalnya takeWhile()mungkin perlu memutuskan kapan harus menghentikan iterasi berdasarkan objek yang sudah bermutasi, sehingga mengupasnya tidak forEach()akan memiliki efek yang sama.

PS Saya belum referensi di map()mana saja karena jika kita ingin bermutasi objek (atau keadaan global), daripada menghasilkan objek baru, ia berfungsi persis seperti peek().

Marinos An
sumber
3

Meskipun saya setuju dengan sebagian besar jawaban di atas, saya punya satu kasus di mana menggunakan mengintip sebenarnya sepertinya cara paling bersih untuk pergi.

Mirip dengan use case Anda, misalkan Anda ingin memfilter hanya pada akun aktif dan kemudian melakukan login pada akun ini.

accounts.stream()
    .filter(Account::isActive)
    .peek(login)
    .collect(Collectors.toList());

Mengintip berguna untuk menghindari panggilan yang tidak perlu sambil tidak mengulangi koleksi dua kali:

accounts.stream()
    .filter(Account::isActive)
    .map(account -> {
        account.login();
        return account;
    })
    .collect(Collectors.toList());
UltimaWeapon
sumber
3
Yang harus Anda lakukan adalah menyelesaikan metode login dengan benar. Aku benar-benar tidak melihat bagaimana mengintip adalah cara paling bersih untuk pergi. Bagaimana seharusnya seseorang yang membaca kode Anda tahu bahwa Anda sebenarnya menyalahgunakan API. Kode yang baik dan bersih tidak memaksa pembaca untuk membuat asumsi tentang kode tersebut.
kaba713
1

Solusi fungsional adalah membuat objek akun tidak berubah. Jadi akun.login () harus mengembalikan objek akun baru. Ini berarti operasi peta dapat digunakan untuk login dan bukan mengintip.

Solubris
sumber