Mengapa findFirst () melontarkan NullPointerException jika elemen pertama yang ditemukannya adalah null?

90

Mengapa lemparan ini java.lang.NullPointerException?

List<String> strings = new ArrayList<>();
        strings.add(null);
        strings.add("test");

        String firstString = strings.stream()
                .findFirst()      // Exception thrown here
                .orElse("StringWhenListIsEmpty");
                //.orElse(null);  // Changing the `orElse()` to avoid ambiguity

Item pertama masuk stringsadalah null, yang merupakan nilai yang dapat diterima dengan sempurna. Selanjutnya, findFirst()mengembalikan sebuah Opsional , yang lebih masuk akal untuk findFirst()dapat menangani nulls.

EDIT: memperbarui orElse()menjadi kurang ambigu.

tidak pernah berakhirqs
sumber
4
null bukanlah nilai yang dapat diterima secara sempurna ... gunakan "" sebagai gantinya
Michele Lacorte
1
@MicheleLacorte meskipun saya menggunakan di Stringsini, bagaimana jika itu adalah daftar yang mewakili kolom di DB? Nilai baris pertama untuk kolom itu bisa null.
Neverendingqs
Ya, tetapi di java null tidak dapat diterima
.. gunakan
10
@MicheleLacorte, nulladalah nilai yang dapat diterima secara sempurna di Java, secara umum. Secara khusus, ini adalah elemen yang valid untuk file ArrayList<String>. Namun, seperti nilai lainnya, ada batasan tentang apa yang dapat dilakukan dengannya. "Jangan pernah menggunakan null" bukanlah nasihat yang berguna, karena Anda tidak dapat menghindarinya.
John Bollinger
@NathanHughes - Saya curiga pada saat Anda menelepon findFirst(), tidak ada lagi yang ingin Anda lakukan.
neverendingqs

Jawaban:

72

Alasan untuk ini adalah penggunaan Optional<T>dalam pengembalian. Opsional tidak boleh mengandung null. Pada dasarnya, ia tidak menawarkan cara untuk membedakan situasi "itu tidak ada" dan "itu ada di sana, tetapi sudah diatur ke null".

Itulah mengapa dokumentasi secara eksplisit melarang situasi saat nulldipilih di findFirst():

Melempar:

NullPointerException - jika elemen yang dipilih adalah null

Sergey Kalinichenko
sumber
3
Ini akan mudah untuk melacak jika nilai memang ada dengan boolean pribadi di dalam instance Optional. Bagaimanapun, saya pikir saya hampir mengomel - jika bahasanya tidak mendukung, itu tidak mendukungnya.
Neverendingqs
2
@neverendingqs Tentu saja, menggunakan a booleanuntuk membedakan kedua situasi ini akan sangat masuk akal. Bagi saya, sepertinya penggunaan di Optional<T>sini adalah pilihan yang dipertanyakan.
Sergey Kalinichenko
1
@neverendingqs Saya tidak bisa memikirkan alternatif yang tampak bagus untuk ini, selain menggulung null Anda sendiri , yang juga tidak ideal.
Sergey Kalinichenko
1
Saya akhirnya menulis metode pribadi yang mendapatkan iterator dari semua Iterablejenis, memeriksa hasNext(), dan mengembalikan nilai yang sesuai.
neverendingqs
1
Saya pikir lebih masuk akal jika findFirstmengembalikan nilai Opsional kosong dalam kasus PO
danny
48

Seperti yang telah dibahas , perancang API tidak berasumsi bahwa pengembang ingin memperlakukan nullnilai dan nilai yang tidak ada dengan cara yang sama.

Jika Anda masih ingin melakukannya, Anda dapat melakukannya secara eksplisit dengan menerapkan urutan

.map(Optional::ofNullable).findFirst().flatMap(Function.identity())

ke sungai. Hasilnya akan menjadi opsional kosong dalam kedua kasus, jika tidak ada elemen pertama atau jika elemen pertama adalah null. Jadi dalam kasus Anda, Anda dapat menggunakan

String firstString = strings.stream()
    .map(Optional::ofNullable).findFirst().flatMap(Function.identity())
    .orElse(null);

untuk mendapatkan nullnilai jika elemen pertama tidak ada atau null.

Jika Anda ingin membedakan kasus-kasus ini, Anda dapat mengabaikan flatMaplangkah berikut:

Optional<String> firstString = strings.stream()
    .map(Optional::ofNullable).findFirst().orElse(null);
System.out.println(firstString==null? "no such element":
                   firstString.orElse("first element is null"));

Ini tidak jauh berbeda dengan pertanyaan Anda yang diperbarui. Anda hanya perlu mengganti "no such element"dengan "StringWhenListIsEmpty"dan "first element is null"dengan null. Tetapi jika Anda tidak menyukai persyaratan, Anda dapat mencapainya juga seperti:

String firstString = strings.stream().skip(0)
    .map(Optional::ofNullable).findFirst()
    .orElseGet(()->Optional.of("StringWhenListIsEmpty"))
    .orElse(null);

Sekarang, firstStringakan menjadi nulljika elemen ada tetapi ada nulldan akan terjadi "StringWhenListIsEmpty"ketika tidak ada elemen.

Holger
sumber
Maaf saya menyadari bahwa pertanyaan saya mungkin tersirat bahwa saya ingin kembali nulluntuk 1) elemen pertama nullatau 2) tidak ada elemen di dalam daftar. Saya telah memperbarui pertanyaan untuk menghilangkan ambiguitas.
Neverendingqs
1
Dalam potongan kode ke-3, Optionalmungkin ditugaskan ke null. Karena Optionalseharusnya menjadi "tipe nilai", itu tidak boleh nol. Dan Opsional tidak boleh dibandingkan dengan ==. Kode mungkin gagal di Java 10 :) atau tipe nilai kapan saja diperkenalkan ke Java.
ZhongYu
1
@ bayou.io: dokumentasi tidak mengatakan bahwa referensi ke tipe nilai tidak boleh nulldan meskipun instance tidak boleh dibandingkan ==, referensi tersebut dapat diuji untuk nulldigunakan ==karena itulah satu - satunya cara untuk mengujinya null. Saya tidak dapat melihat bagaimana transisi ke "tidak pernah null" seharusnya berfungsi untuk kode yang ada karena nilai default untuk semua variabel instance dan elemen array adalah null. Cuplikannya pasti bukan kode terbaik tetapi juga bukan tugas memperlakukan nulls sebagai nilai sekarang.
Holger
lihat john rose - juga tidak dapat dibandingkan dengan operator "==", bahkan dengan nol
ZhongYu
1
Karena kode ini menggunakan Generic API, konsep inilah yang menyebutnya representasi kotak yang bisa jadi null. Namun, karena perubahan bahasa hipotetis seperti itu akan menyebabkan kompiler mengeluarkan kesalahan di sini (tidak memecahkan kode secara diam-diam), saya dapat menerima fakta, bahwa itu mungkin harus diadaptasi untuk Java 10. Saya kira, StreamAPI akan terlihat sangat berbeda saat itu juga ...
Holger
20

Anda dapat menggunakan java.util.Objects.nonNulluntuk memfilter daftar sebelum menemukan

sesuatu seperti

list.stream().filter(Objects::nonNull).findFirst();
Mattos
sumber
1
Saya ingin firstStringmenjadi nulljika item pertama stringsadalah null.
Neverendingqs
3
sayangnya itu menggunakan Optional.ofyang tidak aman nol. Anda bisa mapke Optional.ofNullable dan kemudian menggunakan findFirsttetapi Anda akan berakhir dengan pilihan dari Opsional
Mattos
15

Kode berikut menggantikan findFirst()dengan limit(1)dan menggantikan orElse()dengan reduce():

String firstString = strings.
   stream().
   limit(1).
   reduce("StringWhenListIsEmpty", (first, second) -> second);

limit()memungkinkan hanya 1 elemen untuk dijangkau reduce. The BinaryOperatorpassing to reducemengembalikan 1 elemen itu atau "StringWhenListIsEmpty"jika tidak ada elemen yang mencapai reduce.

Keunggulan dari solusi ini Optionaladalah tidak dialokasikan dan BinaryOperatorlambda tidak akan mengalokasikan apa pun.

Nathan
sumber
1

Opsional seharusnya menjadi tipe "nilai". (baca cetakan kecilnya di javadoc :) JVM bahkan dapat menggantikan semua Optional<Foo>hanya dengan Foo, menghapus semua biaya tinju dan unboxing. A nullFoo artinya kosong Optional<Foo>.

Ini adalah desain yang memungkinkan untuk mengizinkan Opsional dengan nilai null, tanpa menambahkan tanda boolean - cukup tambahkan objek sentinel. (bahkan bisa digunakan thissebagai sentinel; lihat Throwable.cause)

Keputusan bahwa Opsional tidak dapat membungkus null tidak didasarkan pada biaya runtime. Ini adalah masalah yang sangat diperdebatkan dan Anda perlu menggali milis. Keputusan tidak meyakinkan semua orang.

Dalam kasus apa pun, karena Opsional tidak dapat membungkus nilai null, itu mendorong kita ke sudut dalam kasus seperti findFirst. Mereka pasti beralasan bahwa nilai null sangat jarang (bahkan dianggap bahwa Stream harus melarang nilai null), oleh karena itu akan lebih mudah untuk melemparkan pengecualian pada nilai null daripada di streaming kosong.

Solusinya adalah dengan kotak null, misalnya

class Box<T>
    static Box<T> of(T value){ .. }

Optional<Box<String>> first = stream.map(Box::of).findFirst();

(Mereka mengatakan solusi untuk setiap masalah OOP adalah dengan memperkenalkan tipe lain :)

ZhongYu
sumber
1
Tidak perlu membuat Boxtipe lain . The Optionaljenis itu sendiri dapat melayani tujuan ini. Lihat jawaban saya sebagai contoh.
Holger
@Holger - ya, tapi itu mungkin membingungkan karena itu bukan tujuan yang dimaksudkan dari Opsional. Dalam kasus OP, nulladalah nilai yang valid seperti yang lain, tidak ada perlakuan khusus untuk itu. (sampai beberapa saat kemudian :)
ZhongYu