Pertanyaan Di aliran Java adalah intip benar-benar hanya untuk debugging?


Saya membaca tentang aliran Jawa dan menemukan hal-hal baru ketika saya pergi. Salah satu hal baru yang saya temukan adalah peek() fungsi. 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 loggedIn ().

saya juga punya

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

dan

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

Kenapa ini begitu buruk?

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

Sekarang, sejauh yang saya tahu, ini persis seperti apa yang dimaksudkan untuk dilakukan. Saya t;

  • Mengambil daftar akun
  • Mencoba masuk ke setiap akun
  • Menyaring akun apa pun yang tidak masuk
  • Mengumpulkan akun yang sudah masuk ke dalam daftar baru

Apa sisi buruk dari melakukan sesuatu seperti ini? Apa alasannya aku tidak boleh melanjutkan? Terakhir, jika tidak solusi ini lalu apa?

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

.filter(account -> {
        account.login();
        return account.loggedIn();
    })

76
2017-11-10 17:12


asal


Jawaban:


Kunci takeaway dari ini:

Jangan gunakan API dengan cara yang tidak diinginkan, bahkan jika itu menyelesaikan tujuan langsung Anda. Pendekatan itu mungkin rusak di masa depan, dan juga tidak jelas bagi pengelola masa depan.


Tidak ada salahnya memecah hal ini ke beberapa operasi, karena ini adalah operasi yang berbeda. Sana aku s bahaya dalam menggunakan API dengan cara yang tidak jelas dan tidak disengaja, yang mungkin memiliki konsekuensi jika perilaku khusus ini dimodifikasi dalam versi Java di masa mendatang.

Menggunakan forEach pada operasi ini akan menjelaskan kepada pengelola bahwa ada dimaksudkan efek samping pada setiap elemen accounts, dan bahwa Anda melakukan beberapa operasi yang dapat memutasikannya.

Ini juga lebih konvensional dalam artian itu peek adalah operasi perantara yang tidak beroperasi di seluruh koleksi hingga operasi terminal berjalan, tetapi forEach memang operasi terminal. Dengan cara ini, Anda dapat membuat argumen yang kuat di sekitar perilaku dan alur kode Anda sebagai lawan untuk mengajukan pertanyaan tentang apakah peek akan berperilaku sama forEach tidak dalam konteks ini.

accounts.forEach(a -> a.login());
List<Account> loggedInAccounts = accounts.stream()
                                         .filter(Account::loggedIn)
                                         .collect(Collectors.toList());

49
2017-11-10 17:55



Hal penting yang harus Anda pahami, adalah bahwa streaming didorong oleh operasi terminal. Operasi terminal menentukan apakah semua elemen harus diproses atau tidak sama sekali. Begitu collect adalah operasi yang memproses setiap item, sedangkan findAny dapat menghentikan pemrosesan item setelah menemukan elemen yang cocok.

Dan count() tidak dapat memproses elemen apa pun ketika ia dapat menentukan ukuran aliran tanpa memproses item. Karena ini adalah pengoptimalan yang tidak dibuat di Java 8, tetapi yang akan ada di Java 9, mungkin ada kejutan ketika Anda beralih ke Java 9 dan memiliki kode yang mengandalkan count() memproses semua item. Ini juga terhubung ke detail tergantung implementasi lain, mis. bahkan di Java 9, implementasi referensi tidak akan dapat memprediksi ukuran sumber aliran tak terbatas yang digabungkan limit sementara tidak ada batasan mendasar yang mencegah prediksi seperti itu.

Sejak peek memungkinkan "melakukan tindakan yang disediakan pada setiap elemen sebagai elemen yang 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, mis. ingin menerapkan tindakan pada semua elemen. Ia bekerja jika operasi terminal dijamin untuk memproses semua item, tetapi bahkan kemudian, Anda harus yakin bahwa tidak pengembang berikutnya mengubah operasi terminal (atau Anda lupa aspek halus).

Lebih lanjut, sementara aliran menjamin mempertahankan urutan pertemuan untuk kombinasi operasi tertentu bahkan untuk aliran paralel, jaminan ini tidak berlaku peek. Saat mengumpulkan ke dalam daftar, daftar yang dihasilkan akan memiliki urutan yang benar untuk urutan aliran paralel, tetapi peek tindakan dapat dipanggil dalam urutan acak dan bersamaan.

Jadi hal paling berguna yang bisa Anda lakukan peek adalah untuk mengetahui apakah elemen aliran telah diproses yang persis seperti yang dikatakan dokumentasi API:

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


68
2017-11-10 17:50



Mungkin aturan praktisnya adalah bahwa jika Anda menggunakan mengintip di luar skenario "debug", Anda hanya harus melakukannya jika Anda yakin tentang kondisi penyaringan terminating dan intermediate. 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 ke Bar dan memberi tahu mereka semua halo.

Tampaknya 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 berakhir dengan mengulang koleksi dua kali.


9
2017-11-11 12:52



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

Serupa dengan kasus penggunaan Anda, misalkan Anda hanya ingin memfilter pada akun yang aktif dan kemudian melakukan login pada akun ini.

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

Mengintip berguna untuk menghindari panggilan yang berlebihan sementara tidak perlu mengulang koleksi dua kali:

accounts.stream()
    .filter(Account::isActive)
    .map(account -> {
        account.login();
        return account;
    })
    .collect(Collectors.toList());

2
2017-10-26 14:45



Saya akan mengatakan itu peek menyediakan kemampuan untuk desentralisasi kode yang dapat mengubah objek aliran, atau mengubah status global (berdasarkan mereka), bukannya memasukkan semuanya ke dalam fungsi sederhana atau tersusun diteruskan ke metode terminal.

Sekarang pertanyaannya mungkin: kita harus bermutasi objek aliran 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) kemudian peek() aku s pasti tidak hanya untuk keperluan debugging, untuk alasan yang sama itu forEach() tidak hanya untuk keperluan debugging.

Bagi saya ketika memilih antara forEach() dan peek(), adalah memilih yang berikut: Apakah saya ingin potongan kode yang memutasi objek aliran untuk dilampirkan ke composable, atau apakah saya ingin mereka melampirkan langsung ke aliran?

kupikir peek() pasangan akan lebih baik dengan metode java9. misalnya takeWhile() mungkin perlu memutuskan kapan berhenti iterasi berdasarkan objek yang sudah bermutasi, jadi pisahkan forEach() tidak akan memiliki efek yang sama.

P.S. Saya belum direferensikan map() di mana saja karena jika kita ingin bermutasi objek (atau keadaan global), daripada menghasilkan objek baru, ia bekerja persis seperti peek().


1
2018-06-21 14:14