Dlaczego funkcja findFirst () zgłasza wyjątek NullPointerException, jeśli pierwszy znaleziony element ma wartość null?

89

Dlaczego to rzuca 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

Pierwsza pozycja stringsto null, co jest wartością całkowicie akceptowalną. Ponadto findFirst()zwraca parametr Optional , co ma jeszcze większy sens, findFirst()aby móc obsługiwać nulls.

EDYCJA: zaktualizowano, orElse()aby był mniej niejednoznaczny.

neverendingqs
źródło
4
wartość null nie jest całkowicie akceptowalna… zamiast tego użyj „”
Michele Lacorte
1
@MicheleLacorte, chociaż używam Stringtutaj, co jeśli jest to lista reprezentująca kolumnę w bazie danych? Wartość pierwszego wiersza dla tej kolumny może wynosić null.
neverendingqs
Tak, ale w java null nie jest akceptowalne.
Użyj
10
@MicheleLacorte null, ogólnie mówiąc , jest całkowicie akceptowalną wartością w Javie. W szczególności jest to prawidłowy element dla pliku ArrayList<String>. Jednak jak każda inna wartość istnieją ograniczenia dotyczące tego, co można z nią zrobić. „Nigdy nie używaj null” nie jest przydatną radą, ponieważ nie można jej uniknąć.
John Bollinger,
@NathanHughes - Podejrzewam, że zanim zadzwonisz findFirst(), nie chcesz już nic robić.
neverendingqs

Odpowiedzi:

72

Powodem tego jest użycie Optional<T>w zwrocie. Opcjonalne nie może zawierać null. Zasadniczo nie umożliwia rozróżnienia sytuacji „nie ma go” i „jest, ale jest ustawione na null”.

Dlatego dokumentacja jednoznacznie zabrania sytuacji, w której nullwybrano findFirst():

Rzuty:

NullPointerException - jeśli wybrany element to null

Sergey Kalinichenko
źródło
3
Łatwo byłoby prześledzić, czy istnieje wartość z prywatną wartością logiczną wewnątrz instancji Optional. W każdym razie myślę, że graniczę z rantowaniem - jeśli język tego nie obsługuje, to nie obsługuje.
neverendingqs
2
@neverendingqs Oczywiście użycie a booleando rozróżnienia tych dwóch sytuacji miałoby sens. Wydaje mi się, że użycie Optional<T>tutaj było wątpliwym wyborem.
Sergey Kalinichenko
1
@neverendingqs Nie przychodzi mi do głowy żadna ładnie wyglądająca alternatywa dla tego, poza zrolowaniem własnego null , co też nie jest idealne.
Sergey Kalinichenko
1
Skończyło się na napisaniu metody prywatnej, która pobiera iterator z dowolnego Iterabletypu, sprawdza hasNext()i zwraca odpowiednią wartość.
neverendingqs
1
Myślę, że ma to większy sens, jeśli findFirstzwraca pustą wartość opcjonalną w przypadku PO
Danny
47

Jak już wspomniano , projektanci API nie zakładają, że programista chce traktować nullwartości i wartości nieobecne w ten sam sposób.

Jeśli nadal chcesz to zrobić, możesz to zrobić jawnie, stosując sekwencję

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

do strumienia. Wynik będzie pusty opcjonalny w obu przypadkach, jeśli nie ma pierwszego elementu lub jeśli pierwszy element jest null. Więc w twoim przypadku możesz użyć

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

aby uzyskać nullwartość, jeśli pierwszy element jest nieobecny lub null.

Jeśli chcesz rozróżnić te przypadki, możesz po prostu pominąć flatMapkrok:

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

Nie różni się to zbytnio od zaktualizowanego pytania. Po prostu trzeba wymienić "no such element"z "StringWhenListIsEmpty"i "first element is null"z null. Ale jeśli nie lubisz warunków warunkowych, możesz to osiągnąć również w następujący sposób:

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

Teraz firstStringbędzie, nulljeśli element istnieje, ale jest, nulli będzie, "StringWhenListIsEmpty"gdy żaden element nie istnieje.

Holger
źródło
Przepraszam, zdałem sobie sprawę, że moje pytanie mogło sugerować, że chcę zwrócić nullalbo 1) pierwszy element, nullalbo 2) brak elementów na liście. Zaktualizowałem pytanie, aby usunąć niejednoznaczność.
neverendingqs
1
W trzecim fragmencie kodu Optionalmożna przypisać do null. Ponieważ Optionalma być „typem wartości”, nigdy nie powinien być pusty. I nigdy nie należy porównywać opcji ==. Kod może się nie powieść w Javie 10 :) lub kiedy typ wartości zostanie wprowadzony do Javy.
ZhongYu,
1
@ bayou.io: dokumentacja nie mówi, że odniesienia do typów wartości nie mogą być nulli chociaż instancje nigdy nie powinny być porównywane ==, można przetestować odwołanie pod kątem nullużycia, ==ponieważ jest to jedyny sposób, aby go przetestować null. Nie rozumiem, jak takie przejście na „nigdy null” powinno działać dla istniejącego kodu, ponieważ nawet domyślna wartość dla wszystkich zmiennych instancji i elementów tablicy to null. Fragment z pewnością nie jest najlepszym kodem, ale nie jest też zadaniem traktowania nulls jako wartości bieżących.
Holger,
patrz john rose - ani nie można go porównać z operatorem „==”, nawet z wartością null
ZhongYu
1
Ponieważ ten kod używa ogólnego interfejsu API, to właśnie pojęcie nazywa reprezentacją pudełkową, która może być null. Jednakże, ponieważ taka hipotetyczna zmiana języka spowodowałaby, że kompilator wystawiłby tutaj błąd (nie łamałby kodu po cichu), mogę żyć z faktem, że prawdopodobnie musiałby być dostosowany do Javy 10. Przypuszczam, że StreamAPI będzie wtedy też wyglądają zupełnie inaczej…
Holger
18

Możesz użyć java.util.Objects.nonNulldo filtrowania listy przed znalezieniem

coś jak

list.stream().filter(Objects::nonNull).findFirst();
Mattos
źródło
1
Chcę firstStringbyć, nulljeśli pierwszy przedmiot stringsjest null.
neverendingqs
2
niestety używa, Optional.ofco nie jest null bezpieczne. Możesz to mapzrobić, Optional.ofNullable a następnie użyć, findFirstale skończysz z Opcjonalnym lub opcjonalnym
Mattos
14

Następujący tekst zastępuje kod findFirst()z limit(1)i zastępuje orElse()z reduce():

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

limit()pozwala dosięgnąć tylko 1 elementu reduce. BinaryOperatorPrzeszedł do reducedeklaracji, że 1 elementem albo "StringWhenListIsEmpty"jeśli żadne elementy osiągną reduce.

Piękno tego rozwiązania polega na tym, że Optionalnie jest ono przydzielone, a BinaryOperatorlambda niczego nie przydzieli.

Nathan
źródło
1

Opcjonalny ma być typem „wartości”. (czytaj wydruk grzywny w javadoc :) JVM mogą nawet zastąpić wszystko Optional<Foo>z tylko Foo, usuwając wszelkie koszty bokserskie i unboxing. nullFoo oznacza pusty Optional<Foo>.

Możliwy jest projekt zezwalający na Optional z wartością null, bez dodawania flagi boolowskiej - wystarczy dodać obiekt wartownika. (może nawet służyć thisjako wartownik; zobacz Throwable.cuse)

Decyzja, że ​​Optional nie może zawijać wartości null, nie jest oparta na koszcie czasu wykonywania. To był bardzo sporny problem i musisz przeszukać listy mailingowe. Decyzja nie jest przekonująca dla wszystkich.

W każdym razie, ponieważ Optional nie może zawijać wartości null, popycha nas w róg w przypadkach takich jak findFirst. Musieli rozumować, że wartości null są bardzo rzadkie (uważano nawet, że Stream powinien blokować wartości null), dlatego wygodniej jest zgłosić wyjątek do wartości null zamiast do pustych strumieni.

Sposób obejścia problemu to box null, np

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

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

(Mówią, że rozwiązaniem każdego problemu OOP jest wprowadzenie innego typu :)

ZhongYu
źródło
1
Nie ma potrzeby tworzenia innego Boxtypu. Sam Optionaltyp może temu służyć. Zobacz moją odpowiedź jako przykład.
Holger,
@Holger - tak, ale może to być mylące, ponieważ nie jest to zamierzone przeznaczenie Optional. W przypadku OP nulljest to ważna wartość, jak każda inna, bez specjalnego traktowania. (do pewnego czasu później :)
ZhongYu