Perché findFirst () genera un'eccezione NullPointerException se il primo elemento che trova è nullo?


90

Perché questo genera un 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

Il primo elemento stringsè null, che è un valore perfettamente accettabile. Inoltre, findFirst()restituisce un Optional , che ha ancora più senso per findFirst()poter gestire nulls.

EDIT: aggiornato il orElse()per essere meno ambiguo.


4
null non è un valore perfettamente accettabile ... usa "" invece
Michele Lacorte

1
@MicheleLacorte anche se sto usando Stringqui, e se fosse un elenco che rappresenta una colonna nel DB? Il valore della prima riga per quella colonna può essere null.
neverendingqs

Sì, ma in java null non è accettabile .. usa la query per impostare null in db
Michele Lacorte

10
@MicheleLacorte, nullè un valore perfettamente accettabile in Java, in generale. In particolare, è un elemento valido per un file ArrayList<String>. Come ogni altro valore, tuttavia, ci sono limitazioni su ciò che si può fare con esso. "Non usare mai null" non è un consiglio utile, perché non puoi evitarlo.
John Bollinger

@NathanHughes - Ho il sospetto che quando chiami findFirst(), non c'è nient'altro che vuoi fare.
neverendingqs

Risposte:


72

La ragione di ciò è l'uso di Optional<T>nel ritorno. Facoltativo non è consentito contenere null. In sostanza, non offre alcun modo per distinguere le situazioni "non è lì" e "è lì, ma è impostato su null".

Ecco perché la documentazione vieta esplicitamente la situazione in cui nullè selezionato in findFirst():

Lanci:

NullPointerException - se l'elemento selezionato è null


3
Sarebbe semplice tenere traccia se esiste un valore con un booleano privato all'interno delle istanze di Optional. Ad ogni modo, penso di essere al limite dello sproloquio: se la lingua non lo supporta, non lo supporta.
neverendingqs

2
@neverendingqs Assolutamente, usare a booleanper differenziare queste due situazioni avrebbe perfettamente senso. Per me, sembra che l'uso di Optional<T>qui sia stata una scelta discutibile.
Sergey Kalinichenko

1
@neverendingqs Non riesco a pensare a nessuna bella alternativa a questo, oltre a rotolare il tuo null , che non è nemmeno l'ideale.
Sergey Kalinichenko

1
Ho finito per scrivere un metodo privato che ottiene l'iteratore da qualsiasi Iterabletipo, controlla hasNext()e restituisce il valore appropriato.
neverendingqs

1
Penso che abbia più senso se findFirstrestituisce un valore opzionale vuoto nel caso di PO
danny

48

Come già discusso , i progettisti dell'API non presumono che lo sviluppatore voglia trattare nullvalori e valori assenti allo stesso modo.

Se vuoi ancora farlo, puoi farlo esplicitamente applicando la sequenza

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

al flusso. Il risultato sarà un optional vuoto in entrambi i casi, se non c'è il primo elemento o se il primo elemento lo è null. Quindi nel tuo caso, puoi usare

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

per ottenere un nullvalore se il primo elemento è assente o null.

Se vuoi distinguere tra questi casi, puoi semplicemente omettere il flatMappassaggio:

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

Questo non è molto diverso dalla tua domanda aggiornata. Devi solo sostituire "no such element"con "StringWhenListIsEmpty"e "first element is null"con null. Ma se non ti piacciono i condizionali, puoi ottenerlo anche come:

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

Ora, firstStringsarà nullse un elemento esiste ma è nulle sarà "StringWhenListIsEmpty"quando non esiste alcun elemento.


Mi dispiace, mi sono reso conto che la mia domanda potrebbe aver implicato che volevo restituire null1) il primo elemento è nullo 2) non esistono elementi nell'elenco. Ho aggiornato la domanda per rimuovere l'ambiguità.
neverendingqs

1
Nel terzo snippet di codice, è Optionalpossibile assegnare un tag null. Poiché Optionaldovrebbe essere un "tipo di valore", non dovrebbe mai essere nullo. E un Opzionale non dovrebbe mai essere confrontato con ==. Il codice potrebbe non riuscire in Java 10 :) o quando il tipo di valore viene introdotto in Java.
ZhongYu

1
@ bayou.io: la documentazione non dice che i riferimenti ai tipi di valore non possono essere nulle sebbene le istanze non dovrebbero mai essere confrontate da ==, il riferimento può essere testato per l' nullutilizzo in ==quanto è l' unico modo per testarlo null. Non riesco a vedere come una tale transizione a "mai null" dovrebbe funzionare per il codice esistente come anche il valore predefinito per tutte le variabili di istanza e gli elementi dell'array null. Lo snippet sicuramente non è il codice migliore, ma nemmeno il compito di trattare nulls come valori presenti.
Holger

vedi john rose - né può essere confrontato con l'operatore "==", nemmeno con un valore nullo
ZhongYu

1
Poiché questo codice utilizza un'API generica, questo è ciò che il concetto chiama una rappresentazione in box che può essere null. Tuttavia, poiché un tale cambio di lingua ipotetico farebbe sì che il compilatore emetta un errore qui (non rompa il codice in silenzio), posso convivere con il fatto che potrebbe essere necessario adattarlo per Java 10. Suppongo che l' StreamAPI lo farà sembra abbastanza diverso anche allora ...
Holger

20

È possibile utilizzare java.util.Objects.nonNullper filtrare l'elenco prima di trovare

qualcosa di simile a

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

1
Voglio firstStringessere nullse il primo elemento stringsè null.
neverendingqs

3
sfortunatamente usa Optional.ofche non è nullo. Potresti mapfarlo Optional.ofNullable e poi usarlo, findFirstma ti ritroverai con un Opzionale di Opzionale
Mattos

15

Il seguente testo sostituisce il codice findFirst()con limit(1)e sostituisce orElse()con reduce():

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

limit()consente di raggiungere solo 1 elemento reduce. Il BinaryOperatorpassato a reducerestituisce quell'elemento 1 oppure "StringWhenListIsEmpty"se nessun elemento raggiunge il reduce.

La bellezza di questa soluzione è che Optionalnon viene allocata e BinaryOperatorlambda non assegnerà nulla.


1

Facoltativo dovrebbe essere un tipo "valore". (leggi le scritte in piccolo in javadoc :) JVM potrebbe persino sostituire tutto Optional<Foo>con solo Foo, rimuovendo tutti i costi di boxe e unboxing. Un nullFoo significa un vuoto Optional<Foo>.

È possibile che consenta l'Opzionale con un valore nullo, senza aggiungere un flag booleano: basta aggiungere un oggetto sentinel. (potrebbe anche essere usato thiscome sentinella; vedere Throwable.cause)

La decisione che Optional non può eseguire il wrapping di null non è basata sul costo di runtime. Questo è stato un problema molto dibattuto e devi scavare nelle mailing list. La decisione non convince tutti.

In ogni caso, poiché Opzionale non può racchiudere un valore nullo, ci spinge in un angolo in casi come findFirst. Devono aver ragionato sul fatto che i valori null sono molto rari (è stato persino considerato che Stream dovrebbe bloccare i valori null), quindi è più conveniente lanciare un'eccezione sui valori null invece che sui flussi vuoti.

Una soluzione alternativa è inscatolare null, ad es

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

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

(Dicono che la soluzione a ogni problema OOP sia introdurre un altro tipo :)


1
Non è necessario creare un altro Boxtipo. Il Optionaltipo stesso può servire a questo scopo. Vedi la mia risposta per un esempio.
Holger

@Holger - sì, ma potrebbe creare confusione poiché non è lo scopo previsto di Optional. Nel caso di OP, nullè un valore valido come un altro, nessun trattamento speciale per esso. (fino a qualche tempo dopo :)
ZhongYu
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.