Warum löst findFirst () eine NullPointerException aus, wenn das erste gefundene Element null ist?

86

Warum wirft das ein 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

Der erste Punkt in stringsist null, was ein vollkommen akzeptabler Wert ist. Darüber hinaus findFirst()gibt eine Optional , die für noch mehr Sinn macht , findFirst()zu handhaben zu können nulls.

BEARBEITEN: aktualisiert orElse(), um weniger mehrdeutig zu sein.

Neverendingqs
quelle
4
null ist kein vollkommen akzeptabler Wert ... benutze stattdessen ""
Michele Lacorte
1
@MicheleLacorte, obwohl ich Stringhier verwende, was ist, wenn es eine Liste ist, die eine Spalte in der Datenbank darstellt? Der Wert der ersten Zeile für diese Spalte kann sein null.
Neverendingqs
Ja, aber in Java ist null nicht akzeptabel.
Verwenden Sie die
10
@MicheleLacorte nullist im Allgemeinen ein durchaus akzeptabler Wert in Java. Insbesondere ist es ein gültiges Element für eine ArrayList<String>. Wie bei jedem anderen Wert gibt es jedoch Einschränkungen, was damit gemacht werden kann. "Niemals verwenden null" ist kein nützlicher Rat, da Sie ihn nicht vermeiden können.
John Bollinger
@ NathanHughes - Ich vermute, dass findFirst()Sie bis zu Ihrem Anruf nichts mehr tun möchten.
Neverendingqs

Antworten:

70

Der Grund dafür ist die Verwendung Optional<T>in der Rücksendung. Optional darf nicht enthalten sein null. Im Wesentlichen bietet es keine Möglichkeit, Situationen zu unterscheiden, "es ist nicht da" und "es ist da, aber es ist eingestellt auf null".

Aus diesem Grund verbietet die Dokumentation ausdrücklich die Situation, wenn nullausgewählt wird in findFirst():

Würfe:

NullPointerException - wenn das ausgewählte Element ist null

dasblinkenlight
quelle
3
Es wäre einfach zu verfolgen, ob ein Wert mit einem privaten Booleschen Wert in Instanzen von vorhanden ist Optional. Wie auch immer, ich glaube, ich bin fast am Rande des Geschwätzes - wenn die Sprache es nicht unterstützt, unterstützt sie es nicht.
Neverendingqs
2
@neverendingqs Die Verwendung von a booleanzur Unterscheidung dieser beiden Situationen wäre absolut sinnvoll. Für mich scheint die Verwendung von Optional<T>hier eine fragwürdige Wahl zu sein.
Dasblinkenlight
1
@neverendingqs Ich kann mir keine gut aussehende Alternative dazu vorstellen, außer deine eigene Null zu würfeln , was auch nicht ideal ist.
Dasblinkenlight
1
Am Ende habe ich eine private Methode geschrieben, die den Iterator von einem beliebigen IterableTyp abruft , prüft hasNext()und den entsprechenden Wert zurückgibt.
nie endende qs
1
Ich denke, es ist sinnvoller, wenn findFirstin
POs
46

Wie bereits erwähnt , gehen die API-Designer nicht davon aus, dass der Entwickler nullWerte und fehlende Werte gleich behandeln möchte .

Wenn Sie dies dennoch tun möchten, können Sie dies explizit tun, indem Sie die Sequenz anwenden

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

zum Strom. Das Ergebnis ist in beiden Fällen leer, wenn kein erstes Element vorhanden ist oder wenn das erste Element vorhanden ist null. In Ihrem Fall können Sie also verwenden

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

um einen nullWert zu erhalten , wenn das erste Element fehlt oder null.

Wenn Sie zwischen diesen Fällen unterscheiden möchten, können Sie das einfach weglassen flatMap Schritt :

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

Dies unterscheidet sich nicht wesentlich von Ihrer aktualisierten Frage. Sie müssen nur "no such element"mit "StringWhenListIsEmpty"und "first element is null"mit ersetzennull . Aber wenn Sie Bedingungen nicht mögen, können Sie es auch erreichen wie:

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

Nun firstStringwird es sein, nullwenn ein Element existiert, aber es ist nullund es wird sein, "StringWhenListIsEmpty"wenn kein Element existiert.

Holger
quelle
Es tut mir leid, dass ich festgestellt habe, dass meine Frage möglicherweise impliziert hat, dass ich nullentweder für 1) das erste Element nulloder 2) keine Elemente in der Liste zurückkehren möchte . Ich habe die Frage aktualisiert, um die Mehrdeutigkeit zu beseitigen.
Neverendingqs
1
Im 3. Code-Snippet Optionalkann ein zugewiesen werden null. Da Optionales sich um einen "Werttyp" handeln soll, sollte er niemals null sein. Und ein Optional sollte niemals von verglichen werden ==. Der Code kann in Java 10 fehlschlagen :) oder wenn ein Werttyp in Java eingeführt wird.
ZhongYu
1
@ bayou.io: Die Dokumentation besagt nicht, dass Verweise auf Werttypen nicht möglich sind, nullund während Instanzen niemals verglichen werden sollten ==, kann die Referenz auf ihre nullVerwendung getestet werden ==, da dies die einzige Möglichkeit ist, sie zu testen null. Ich kann nicht sehen, wie ein solcher Übergang zu "nie null" für vorhandenen Code funktionieren sollte, da selbst der Standardwert für alle Instanzvariablen und Array-Elemente ist null. Das Snippet ist sicherlich nicht der beste Code, aber es ist auch nicht die Aufgabe, nulls als aktuelle Werte zu behandeln.
Holger
siehe John Rose - noch kann es mit dem Operator "==" verglichen werden, nicht einmal mit einer Null
ZhongYu
1
Da dieser Code eine generische API verwendet, nennt das Konzept eine Boxed Representation, die sein kann null. Da eine solche hypothetische Sprachänderung jedoch dazu führen würde, dass der Compiler hier einen Fehler ausgibt (den Code nicht stillschweigend bricht), kann ich damit leben, dass er möglicherweise für Java 10 angepasst werden müsste. Ich nehme an, die StreamAPI wird dies tun sehen dann auch ganz anders aus…
Holger
15

Sie können java.util.Objects.nonNulldie Liste vor dem Suchen filtern

etwas wie

list.stream().filter(Objects::nonNull).findFirst();
Mattos
quelle
1
Ich möchte firstStringsein, nullwenn der erste Artikel in stringsist null.
Neverendingqs
2
leider verwendet es Optional.ofwas nicht null sicher ist. Sie könnten mapzu Optional.ofNullable und dann verwenden, findFirstaber Sie werden mit einem Optional von Optional
Mattos
14

Der folgende Code wird findFirst()durch limit(1)und ersetzt orElse()durch reduce():

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

limit()lässt nur 1 Element zu erreichen reduce. Das BinaryOperatorübergeben an reducegibt das 1 Element oder sonst zurück"StringWhenListIsEmpty" wenn keine Elemente das erreichen reduce.

Das Schöne an dieser Lösung ist, dass sie Optionalnicht zugewiesen wird und das BinaryOperatorLambda nichts zuweisen wird.

Nathan
quelle
1

Optional soll ein "Wert" -Typ sein. (Lesen Sie das Kleingedruckte in Javadoc :) JVM könnte sogar alle Optional<Foo>durch nur ersetzen und alle FooKosten für das Ein- und Auspacken beseitigen. Ein nullFoo bedeutet leer Optional<Foo>.

Es ist möglich, Optional mit einem Nullwert zuzulassen, ohne ein boolesches Flag hinzuzufügen. Fügen Sie einfach ein Sentinel-Objekt hinzu. (könnte sogar gebrauchenthis als Sentinel verwendet werden; siehe Throwable.cause)

Die Entscheidung, dass Optional nicht null umbrechen kann, basiert nicht auf den Laufzeitkosten. Dies war ein äußerst umstrittenes Problem, und Sie müssen die Mailinglisten durchsuchen. Die Entscheidung überzeugt nicht alle.

Da Optional den Nullwert nicht umbrechen kann, werden wir in Fällen wie z findFirst . Sie müssen begründet haben, dass Nullwerte sehr selten sind (es wurde sogar in Betracht gezogen, dass Stream Nullwerte sperren sollte), daher ist es bequemer, Ausnahmen für Nullwerte anstatt für leere Streams auszulösen.

Eine Problemumgehung besteht darin null, z

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

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

(Sie sagen, dass die Lösung für jedes OOP-Problem darin besteht, einen anderen Typ einzuführen :)

ZhongYu
quelle
1
Es ist nicht erforderlich, einen anderen BoxTyp zu erstellen . Der OptionalTyp selbst kann diesem Zweck dienen. Siehe meine Antwort für ein Beispiel.
Holger
@Holger - ja, aber das könnte verwirrend sein, da es nicht der beabsichtigte Zweck von Optional ist. Im Fall von OP nullist ein gültiger Wert wie jeder andere keine besondere Behandlung dafür. (bis
einige Zeit