Mengapa Optional.get () tanpa memanggil isPresent () buruk, tetapi tidak iterator.next ()?

23

Saat menggunakan Java8 stream api baru untuk menemukan satu elemen spesifik dalam koleksi, saya menulis kode seperti ini:

    String theFirstString = myCollection.stream()
            .findFirst()
            .get();

Di sini IntelliJ memperingatkan bahwa get () dipanggil tanpa memeriksa isPresent () terlebih dahulu.

Namun, kode ini:

    String theFirstString = myCollection.iterator().next();

... tidak menghasilkan peringatan.

Apakah ada beberapa perbedaan mendasar antara kedua teknik, yang membuat pendekatan streaming lebih "berbahaya" dalam beberapa cara, bahwa sangat penting untuk tidak pernah memanggil get () tanpa memanggil isPresent () terlebih dahulu? Googling sekitar, saya menemukan artikel yang berbicara tentang bagaimana programmer ceroboh dengan Opsional <> dan menganggap mereka dapat memanggil get () kapan saja.

Masalahnya, saya ingin mendapatkan pengecualian yang dilemparkan ke saya sedekat mungkin ke tempat kode saya bermasalah, yang dalam kasus ini adalah tempat saya memanggil get () ketika tidak ada elemen yang ditemukan. Saya tidak ingin harus menulis esai yang tidak berguna seperti:

    Optional<String> firstStream = myCollection.stream()
            .findFirst();
    if (!firstStream.isPresent()) {
        throw new IllegalStateException("There is no first string even though I totally expected there to be! <sarcasm>This exception is much more useful than NoSuchElementException</sarcasm>");
    }
    String theFirstString = firstStream.get();

... kecuali ada bahaya dalam memprovokasi pengecualian dari get () yang tidak saya sadari?


Setelah membaca respons dari Karl Bielefeldt dan komentar dari Hulk, saya menyadari kode pengecualian saya di atas agak canggung, berikut ini sesuatu yang lebih baik:

    String errorString = "There is no first string even though I totally expected there to be! <sarcasm>This exception is much more useful than NoSuchElementException</sarcasm>";
    String firstString = myCollection.stream()
            .findFirst()
            .orElseThrow(() -> new IllegalStateException(errorString));

Ini terlihat lebih bermanfaat dan mungkin akan menjadi alami di banyak tempat. Saya masih merasa seperti saya ingin dapat memilih satu elemen dari daftar tanpa harus berurusan dengan semua ini, tapi itu bisa saja saya perlu membiasakan diri dengan konstruksi semacam ini.

TV Frank
sumber
7
Pengecualian bagian hrowing dari contoh terakhir Anda dapat ditulis lebih singkat jika Anda menggunakan orElseThrow - dan itu akan menjadi jelas bagi semua pembaca bahwa Anda bermaksud pengecualian dilemparkan.
Hulk

Jawaban:

12

Pertama-tama, pertimbangkan bahwa pemeriksaan itu rumit dan mungkin memakan waktu relatif lama untuk dilakukan. Ini memerlukan beberapa analisis statis, dan mungkin ada sedikit kode antara dua panggilan.

Kedua, penggunaan idiomatik kedua konstruk ini sangat berbeda. Saya memprogram penuh waktu di Scala, yang menggunakan Optionsjauh lebih sering daripada Java. Saya tidak dapat mengingat satu pun contoh di mana saya perlu menggunakan .get()pada Option. Anda hampir selalu harus mengembalikan Optionaldari fungsi Anda, atau menggunakan orElse()sebagai gantinya. Ini adalah cara yang jauh lebih unggul untuk menangani nilai-nilai yang hilang daripada melemparkan pengecualian.

Karena .get()jarang disebut dalam penggunaan normal, masuk akal jika IDE memulai pemeriksaan rumit ketika melihat .get()untuk melihat apakah itu digunakan dengan benar. Sebaliknya, iterator.next()adalah penggunaan iterator yang tepat dan di mana-mana.

Dengan kata lain, ini bukan masalah satu menjadi buruk dan yang lain tidak, itu masalah satu menjadi kasus umum yang mendasar bagi operasinya dan sangat mungkin untuk melemparkan pengecualian selama pengujian pengembang, dan yang lainnya jarang digunakan kasus yang mungkin tidak cukup dilaksanakan untuk melemparkan pengecualian selama pengujian pengembang. Itulah jenis kasus yang Anda ingin agar IDE memperingatkan Anda.

Karl Bielefeldt
sumber
Bisa jadi saya perlu belajar lebih banyak tentang bisnis opsional ini - Saya sering melihat hal-hal java8 sebagai cara yang baik untuk melakukan pemetaan daftar objek yang kami lakukan banyak di webapp kami. Jadi, Anda seharusnya mengirim Opsional <> di mana-mana? Itu akan membutuhkan waktu untuk membiasakan diri, tetapi itu adalah pos SE lainnya! :)
TV Frank
@ TV's Frank Ini kurang bahwa mereka seharusnya ada di mana-mana, dan lebih dari itu mereka memungkinkan Anda untuk menulis fungsi yang mengubah nilai dari tipe yang terkandung, dan Anda rantai mereka dengan mapatau flatMap. OptionalSebagian besar cara yang bagus untuk menghindari harus mengurung semuanya dengan nullcek.
Morgen
15

Kelas opsional dimaksudkan untuk digunakan ketika tidak diketahui apakah item yang dikandungnya ada atau tidak. Peringatan itu ada untuk memberi tahu programmer bahwa ada jalur kode tambahan yang mungkin dapat mereka tangani dengan baik. Idenya adalah untuk menghindari pengecualian. Case use yang Anda presentasikan bukan untuk apa dirancang, dan oleh karena itu peringatannya tidak relevan bagi Anda - jika Anda benar-benar ingin pengecualian dilemparkan, lanjutkan dan nonaktifkan (meskipun hanya untuk baris ini, bukan secara global - Anda harus membuat keputusan apakah akan menangani kasus tidak hadir atas dasar contoh per contoh, dan peringatan akan mengingatkan Anda untuk melakukan ini.

Dapat diperdebatkan, peringatan serupa harus dihasilkan untuk iterator. Namun, itu tidak pernah dilakukan, dan menambahkan satu sekarang mungkin akan menyebabkan terlalu banyak peringatan dalam proyek yang ada menjadi ide yang baik.

Perhatikan juga bahwa melempar pengecualian Anda sendiri bisa lebih bermanfaat daripada hanya pengecualian default, karena termasuk deskripsi tentang elemen apa yang diharapkan ada tetapi tidak bisa sangat membantu dalam debugging di kemudian hari.

Jules
sumber
6

Saya setuju bahwa tidak ada perbedaan yang melekat dalam hal ini, namun, saya ingin menunjukkan kelemahan yang lebih besar.

Dalam kedua kasus, Anda memiliki tipe, yang menyediakan metode yang tidak dijamin berfungsi dan Anda harus memanggil metode lain sebelum itu. Apakah IDE / kompiler / etc Anda memperingatkan Anda tentang satu case atau yang lain hanya tergantung pada apakah ia mendukung case ini dan / atau Anda telah mengonfigurasinya untuk melakukannya.

Namun demikian, kedua kode tersebut adalah kode bau. Ungkapan-ungkapan ini mengisyaratkan cacat desain yang mendasarinya dan menghasilkan kode rapuh.

Anda memang benar ingin gagal puasa tentu saja, tetapi solusinya setidaknya bagi saya, tidak bisa hanya mengangkat bahu dan melemparkan tangan Anda ke udara (atau pengecualian ke tumpukan).

Koleksi Anda mungkin diketahui tidak kosong untuk saat ini, tetapi yang benar-benar dapat Anda andalkan adalah jenisnya: Collection<T>- dan itu tidak menjamin Anda tidak memiliki kehampaan. Meskipun Anda tahu sekarang menjadi tidak kosong, persyaratan yang diubah dapat menyebabkan kode dimuka diubah dan tiba-tiba kode Anda terkena koleksi kosong.

Sekarang Anda bebas untuk mendorong balik dan memberi tahu orang lain bahwa Anda seharusnya mendapatkan daftar yang tidak kosong. Anda bisa senang bahwa Anda menemukan 'kesalahan' itu dengan cepat. Namun demikian, Anda memiliki perangkat lunak yang rusak dan perlu mengubah kode Anda.

Bandingkan ini dengan pendekatan yang lebih pragmatis: Karena Anda mendapatkan koleksi dan mungkin kosong, Anda memaksakan diri untuk menangani kasing tersebut dengan menggunakan # map opsional, #orElse, atau metode lainnya, kecuali #get.

Kompiler bekerja sebanyak mungkin untuk Anda sehingga Anda dapat mengandalkan sebanyak mungkin pada kontrak tipe statis untuk mendapatkan Collection<T>. Ketika kode Anda tidak pernah melanggar kontrak ini (seperti melalui salah satu dari dua rantai panggilan bau), kode Anda jauh lebih rapuh terhadap perubahan kondisi lingkungan.

Dalam skenario di atas, perubahan yang menghasilkan koleksi input kosong tidak akan berdampak apa pun untuk kode Anda. Ini hanyalah input yang valid dan karenanya masih ditangani dengan benar.

Biaya ini adalah bahwa Anda harus menginvestasikan waktu ke desain yang lebih baik, sebagai lawan dari a) mempertaruhkan kode rapuh atau b) hal-hal rumit yang tidak perlu dengan melemparkan pengecualian.

Pada catatan akhir: karena tidak semua IDE / kompiler memperingatkan tentang iterator (). Next () atau opsional.get () panggilan tanpa pemeriksaan sebelumnya, kode tersebut umumnya ditandai dalam ulasan. Untuk apa nilainya, saya tidak akan membiarkan kode seperti itu memungkinkan untuk lulus ulasan dan menuntut solusi bersih sebagai gantinya.

jujur
sumber
Saya kira saya bisa setuju sedikit tentang bau kode - Saya membuat contoh di atas untuk membuat posting pendek. Kasus lain yang lebih valid mungkin adalah dengan memetik elemen wajib dari daftar, yang bukan hal yang aneh untuk muncul dalam aplikasi IMO.
TV Frank
Spot on. "Pilih elemen dengan properti p dari daftar" -> list.stream (). Filter (p) .findFirst () .. dan lagi-lagi kita dipaksa untuk mengajukan pertanyaan "bagaimana jika tidak ada di sana?" .. roti dan mentega setiap hari. Jika itu mandatoray dan "hanya di sana" - mengapa Anda menyembunyikannya di banyak hal lainnya?
Frank
4
Karena mungkin itu daftar yang diambil dari database. Mungkin daftar ini adalah kumpulan statistik yang dikumpulkan dalam beberapa fungsi lain: kita tahu kita memiliki objek A, B dan C, saya ingin mencari jumlah apa pun untuk B. Banyak kasus yang valid (dan dalam aplikasi saya, umum).
TV Frank
Saya tidak tahu tentang database Anda, tetapi milik saya tidak menjamin setidaknya satu hasil per permintaan. Sama untuk koleksi statistik - siapa yang mengatakan bahwa mereka akan selalu kosong? Saya setuju, bahwa ini adalah alasan sederhana bahwa harus ada elemen ini atau itu, namun saya lebih suka mengetik statis yang tepat atas dokumentasi atau lebih buruk beberapa wawasan dari sebuah diskusi.
Frank