Tại sao findFirst () ném NullPointerException nếu phần tử đầu tiên nó tìm thấy là null?


89

Tại sao điều này ném một 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

Mục đầu tiên stringsnull, đó là một giá trị hoàn toàn chấp nhận được. Hơn nữa, findFirst()trả về một Tùy chọn , điều này thậm chí còn có ý nghĩa hơn findFirst()để có thể xử lý nulls.

EDIT: đã cập nhật orElse()để bớt mơ hồ hơn.


4
rỗng không phải là giá trị hoàn toàn chấp nhận được ... sử dụng "" thay vì
Michele Lacorte

1
@MicheleLacorte mặc dù tôi đang sử dụng Stringở đây, nếu đó là danh sách đại diện cho một cột trong DB thì sao? Giá trị của hàng đầu tiên cho cột đó có thể là null.
neverendingqs

Có, nhưng trong java rỗng không phải là acceptable..use truy vấn để thiết lập rỗng vào db
Michele Lacorte

10
@MicheleLacorte, nói chung nulllà một giá trị hoàn toàn chấp nhận được trong Java. Đặc biệt, nó là một phần tử hợp lệ cho một ArrayList<String>. Tuy nhiên, giống như bất kỳ giá trị nào khác, có những hạn chế về những gì có thể được thực hiện với nó. "Đừng bao giờ sử dụng null" không phải là lời khuyên hữu ích, vì bạn không thể tránh nó.
John Bollinger

@NathanHughes - Tôi nghi ngờ rằng vào lúc bạn gọi findFirst(), bạn không muốn làm gì khác.
neverendingqs

Câu trả lời:


72

Lý do cho điều này là việc sử dụng Optional<T>trong trở lại. Tùy chọn không được phép chứa null. Về cơ bản, nó không có cách nào để phân biệt các tình huống "nó không ở đó" và "nó ở đó, nhưng nó được đặt thành null".

Đó là lý do tại sao tài liệu cấm hoàn toàn tình huống nullđược chọn trong findFirst():

Ném:

NullPointerException - nếu phần tử được chọn là null


3
Sẽ thật đơn giản để theo dõi xem một giá trị có tồn tại với một boolean riêng bên trong các phiên bản của Optional. Dù sao đi nữa, tôi nghĩ rằng tôi đang gặp khó khăn - nếu ngôn ngữ không hỗ trợ nó, nó sẽ không hỗ trợ nó.
neverendingqs

2
@neverendingqs Tuyệt đối, sử dụng một booleanđể phân biệt hai tình huống này sẽ rất hợp lý. Đối với tôi, có vẻ như việc sử dụng Optional<T>ở đây là một lựa chọn có vấn đề.
Sergey Kalinichenko

1
@neverendingqs Tôi không thể nghĩ ra bất kỳ giải pháp thay thế đẹp mắt nào cho cái này, ngoài việc cuộn null của riêng bạn , điều này cũng không lý tưởng.
Sergey Kalinichenko

1
Tôi đã kết thúc bằng cách viết một phương thức riêng tư để lấy trình vòng lặp từ bất kỳ Iterableloại nào , kiểm tra hasNext()và trả về giá trị thích hợp.
neverendingqs

1
Tôi nghĩ sẽ có ý nghĩa hơn nếu findFirsttrả về giá trị Tùy chọn trống trong trường hợp của PO
danny

47

Như đã thảo luận , các nhà thiết kế API không cho rằng nhà phát triển muốn xử lý nullcác giá trị và giá trị vắng mặt theo cùng một cách.

Nếu bạn vẫn muốn làm điều đó, bạn có thể làm điều đó một cách rõ ràng bằng cách áp dụng trình tự

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

đến suối. Kết quả sẽ là một tùy chọn trống trong cả hai trường hợp, nếu không có phần tử đầu tiên hoặc nếu phần tử đầu tiên là null. Vì vậy, trong trường hợp của bạn, bạn có thể sử dụng

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

để nhận nullgiá trị nếu phần tử đầu tiên vắng mặt hoặc null.

Nếu bạn muốn phân biệt giữa các trường hợp này, bạn có thể chỉ cần bỏ qua flatMapbước:

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"));

Điều này không khác nhiều với câu hỏi cập nhật của bạn. Bạn chỉ cần thay thế "no such element"bằng "StringWhenListIsEmpty""first element is null"bằng null. Nhưng nếu bạn không thích điều kiện, bạn có thể đạt được nó cũng như:

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

Bây giờ, firstStringsẽ là nullnếu một phần tử tồn tại nhưng tồn tại nullvà nó sẽ là "StringWhenListIsEmpty"khi không có phần tử nào tồn tại.


Xin lỗi, tôi nhận ra rằng câu hỏi của tôi có thể ngụ ý rằng tôi muốn trả lại nullcho 1) phần tử đầu tiên là nullhoặc 2) không có phần tử nào tồn tại trong danh sách. Tôi đã cập nhật câu hỏi để loại bỏ sự mơ hồ.
neverendingqs

1
Trong đoạn mã thứ 3, một Optionalcó thể được gán cho null. Vì Optionalđược cho là một "kiểu giá trị", nó không bao giờ được rỗng. Và một tùy chọn không bao giờ được so sánh bởi ==. Mã có thể bị lỗi trong Java 10 :) hoặc bất cứ khi nào loại giá trị được đưa vào Java.
ZhongYu

1
@ bayou.io: tài liệu không nói rằng không thể có tham chiếu đến các loại giá trị nullvà trong khi các trường hợp không bao giờ được so sánh với nhau ==, tham chiếu có thể được kiểm tra để nullsử dụng ==vì đó là cách duy nhất để kiểm tra nó null. Tôi không thể thấy cách chuyển đổi như vậy thành "không bao giờ null" sẽ hoạt động như thế nào đối với mã hiện có vì ngay cả giá trị mặc định cho tất cả các biến phiên bản và phần tử mảng là như vậy null. Đoạn mã chắc chắn không phải là mã tốt nhất nhưng cũng không có nhiệm vụ coi nullcác s là giá trị hiện tại.
Holger

thấy john tăng - cũng không thể được so sánh với “==” nhà điều hành, thậm chí không có một null
Zhongyu

1
Vì mã này đang sử dụng API chung, nên đây là cái mà khái niệm này gọi là biểu diễn đóng hộpnull . Tuy nhiên, vì sự thay đổi ngôn ngữ giả định như vậy sẽ khiến trình biên dịch phát ra lỗi ở đây (không phải ngắt mã một cách âm thầm), tôi có thể sống với thực tế rằng nó có thể sẽ phải được điều chỉnh cho Java 10. Tôi cho rằng, StreamAPI sẽ sau đó trông cũng khá khác…
Holger

18

Bạn có thể sử dụng java.util.Objects.nonNullđể lọc danh sách trước khi tìm

cái gì đó như

list.stream().filter(Objects::nonNull).findFirst();

1
Tôi muốn firstStringtrở thành nullnếu mục đầu tiên stringsnull.
neverendingqs

2
tiếc là nó sử dụng Optional.ofkhông an toàn. Bạn có thể mapđến Optional.ofNullable và sau đó sử dụng findFirstnhưng bạn sẽ kết thúc với một tùy chọn của tùy chọn
Mattos

14

Đoạn mã sau thay thế findFirst()bằng limit(1)và thay thế orElse()bằng reduce():

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

limit()chỉ cho phép 1 phần tử tiếp cận reduce. Giá trị BinaryOperatorđược truyền để reducetrả về 1 phần tử đó hoặc "StringWhenListIsEmpty"nếu không có phần tử nào đạt đến reduce.

Cái hay của giải pháp này là nó Optionalkhông được phân bổ và BinaryOperatorlambda sẽ không phân bổ bất cứ thứ gì.


1

Tùy chọn được cho là một loại "giá trị". (đọc bản in đẹp trong javadoc :) JVM thậm chí có thể thay thế tất cả Optional<Foo>bằng chỉ Foo, loại bỏ tất cả chi phí quyền anh và mở hộp. Một nullFoo nghĩa là trống rỗng Optional<Foo>.

Đây là một thiết kế khả thi để cho phép Tùy chọn với giá trị null mà không cần thêm cờ boolean - chỉ cần thêm một đối tượng sentinel. (thậm chí có thể sử dụng thislàm lính canh; xem Throwable.cause)

Quyết định rằng Tùy chọn không thể bọc null không dựa trên chi phí thời gian chạy. Đây là một vấn đề vô cùng gay gắt và bạn cần phải tìm hiểu danh sách gửi thư. Quyết định này không thuyết phục tất cả mọi người.

Trong mọi trường hợp, vì Tùy chọn không thể bọc giá trị null, nên nó đẩy chúng ta vào một góc trong các trường hợp như findFirst. Họ phải lý giải rằng giá trị null là rất hiếm (thậm chí còn được coi là Stream phải chặn các giá trị null), do đó sẽ thuận tiện hơn nếu ném ngoại lệ trên các giá trị null thay vì trên các luồng trống.

Một cách giải quyết là đóng hộp null, ví dụ:

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

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

(Họ nói rằng giải pháp cho mọi vấn đề OOP là giới thiệu một loại khác :)


1
Không cần phải tạo một Boxloại khác . Bản Optionalthân loại có thể phục vụ mục đích này. Xem câu trả lời của tôi cho một ví dụ.
Holger

@Holger - có, nhưng điều đó có thể gây nhầm lẫn vì nó không phải là mục đích dự kiến ​​của Tùy chọn. Trong trường hợp của OP, nulllà một giá trị hợp lệ như bất kỳ giá trị nào khác, không có cách xử lý đặc biệt nào cho nó. (cho đến một lúc sau :)
ZhongYu
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.