Por que findFirst () lança um NullPointerException se o primeiro elemento que encontra é nulo?

90

Por que isso lança um 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

O primeiro item stringsé null, que é um valor perfeitamente aceitável. Além disso, findFirst()retorna um Optional , o que faz ainda mais sentido para findFirst()poder manipular nulls.

EDIT: atualizado orElse()para ser menos ambíguo.

neverendingqs
fonte
4
null não é um valor perfeitamente aceitável ... use "" em vez
Michele Lacorte
1
@MicheleLacorte embora eu esteja usando Stringaqui, e se for uma lista que representa uma coluna no DB? O valor da primeira linha dessa coluna pode ser null.
neverendingqs de
Sim, mas em java nulo não é aceitável ... use a consulta para definir nulo no banco de dados
Michele Lacorte
10
@MicheleLacorte, nullé um valor perfeitamente aceitável em Java, em geral. Em particular, é um elemento válido para um ArrayList<String>. Como qualquer outro valor, entretanto, há limitações sobre o que pode ser feito com ele. "Nunca use null" não é um conselho útil, pois você não pode evitá-lo.
John Bollinger,
@NathanHughes - Suspeito que, quando você ligar findFirst(), não há mais nada que queira fazer.
neverendingqs

Respostas:

72

A razão para isso é o uso de Optional<T>na devolução. Opcional não pode conter null. Essencialmente, ele não oferece nenhuma maneira de distinguir situações "não está lá" e "está lá, mas está definido para null".

É por isso que a documentação proíbe explicitamente a situação quando nullé selecionado em findFirst():

Lança:

NullPointerException - se o elemento selecionado for null

Sergey Kalinichenko
fonte
3
Seria simples rastrear se um valor existe com um booleano privado dentro de instâncias de Optional. De qualquer forma, acho que estou quase reclamando - se a linguagem não suporta, não suporta.
neverendingqs
2
@neverendingqs Com certeza, usar um booleanpara diferenciar essas duas situações faria todo o sentido. Para mim, parece que usar Optional<T>aqui foi uma escolha questionável.
Sergey Kalinichenko,
1
@neverendingqs Não consigo pensar em nenhuma alternativa bonita para isso, além de rolar seu próprio null , o que também não é ideal.
Sergey Kalinichenko,
1
Acabei escrevendo um método privado que obtém o iterador de qualquer Iterabletipo, verifica hasNext()e retorna o valor apropriado.
neverendingqs
1
Acho que faz mais sentido se findFirstretornar um valor Opcional vazio no caso de PO
danny
48

Conforme já discutido , os designers de API não presumem que o desenvolvedor deseja tratar nullvalores e valores ausentes da mesma maneira.

Se ainda quiser fazer isso, você pode fazê-lo explicitamente aplicando a sequência

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

para o riacho. O resultado será um opcional vazio em ambos os casos, se não houver um primeiro elemento ou se o primeiro elemento for null. Então, no seu caso, você pode usar

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

para obter um nullvalor se o primeiro elemento estiver ausente ou null.

Se você quiser distinguir entre esses casos, pode simplesmente omitir a flatMapetapa:

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

Isso não é muito diferente da sua pergunta atualizada. Você apenas tem que substituir "no such element"por "StringWhenListIsEmpty"e "first element is null"por null. Mas se você não gosta de condicionais, também pode obtê-lo como:

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

Agora, firstStringserá nullse um elemento existe, mas é nulle será "StringWhenListIsEmpty"quando nenhum elemento existir.

Holger
fonte
Desculpe, percebi que minha pergunta pode ter implicado que eu queria retornar nullpara 1) o primeiro elemento é nullou 2) nenhum elemento existe dentro da lista. Eu atualizei a pergunta para remover a ambigüidade.
neverendingqs
1
No terceiro trecho de código, um Optionalpode ser atribuído a null. Como Optionalé suposto ser um "tipo de valor", nunca deve ser nulo. E um opcional nunca deve ser comparado por ==. O código pode falhar no Java 10 :) ou sempre que o tipo de valor for introduzido no Java.
ZhongYu
1
@ bayou.io: a documentação não diz que as referências aos tipos de valor não podem ser nulle, embora as instâncias nunca devam ser comparadas por ==, a referência pode ser testada para nulluso, ==pois é a única maneira de testá-la null. Não consigo ver como essa transição para “nunca null” deve funcionar para o código existente, pois é até o valor padrão para todas as variáveis ​​de instância e elementos de array null. O snippet certamente não é o melhor código, mas também não é a tarefa de tratar nulls como valores presentes.
Holger,
ver john rose - nem pode ser comparado com o operador “==”, nem mesmo com um nulo
ZhongYu
1
Uma vez que esse código está usando uma API genérica, isso é o que o conceito chama de representação em caixa que pode ser null. No entanto, uma vez que tal mudança hipotética de linguagem faria com que o compilador emitisse um erro aqui (não quebrar o código silenciosamente), posso aceitar o fato de que possivelmente teria que ser adaptado para Java 10. Suponho que a StreamAPI irá parece bem diferente então ...
Holger
20

Você pode usar java.util.Objects.nonNullpara filtrar a lista antes de encontrar

algo como

list.stream().filter(Objects::nonNull).findFirst();
Mattos
fonte
1
Eu quero firstStringser nullse o primeiro item stringsfor null.
neverendingqs de
3
infelizmente ele usa o Optional.ofque não é seguro nulo. Você poderia mappara Optional.ofNullable e, em seguida, usar findFirst, mas você vai acabar com um opcional de Opcional
Mattos
15

O texto seguinte substitui código findFirst()com limit(1)e substitui orElse()com reduce():

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

limit()permite que apenas 1 elemento alcance reduce. O BinaryOperatorpassado para reduceretorna aquele 1 elemento ou então, "StringWhenListIsEmpty"se nenhum elemento alcançar o reduce.

A beleza dessa solução é que Optionalnão está alocado e o BinaryOperatorlambda não vai alocar nada.

Nathan
fonte
1

Opcional deve ser um tipo de "valor". (leia as letras miúdas em javadoc :) JVM poderia até mesmo substituir tudo Optional<Foo>por apenas Foo, removendo todos os custos de encaixotamento e desembalagem. Um nullFoo significa um vazio Optional<Foo>.

É um design possível para permitir Optional com valor nulo, sem adicionar um sinalizador booleano - basta adicionar um objeto sentinela. (pode até ser usado thiscomo sentinela; consulte Throwable.cause)

A decisão de que Opcional não pode agrupar nulo não é baseada no custo de tempo de execução. Este foi um problema muito discutido e você precisa cavar as listas de discussão. A decisão não convence a todos.

Em qualquer caso, uma vez que Optional não pode envolver um valor nulo, ele nos empurra para um canto em casos como findFirst. Eles devem ter raciocinado que os valores nulos são muito raros (foi até considerado que o Stream deveria barrar os valores nulos), portanto, é mais conveniente lançar uma exceção em valores nulos em vez de em fluxos vazios.

Uma solução alternativa é encaixotar null, por exemplo

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

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

(Eles dizem que a solução para cada problema OOP é introduzir outro tipo :)

ZhongYu
fonte
1
Não há necessidade de criar outro Boxtipo. O Optionalpróprio tipo pode servir a esse propósito. Veja minha resposta para um exemplo.
Holger,
@Holger - sim, mas isso pode ser confuso, pois não é o propósito pretendido de Opcional. No caso do OP, nullé um valor válido como qualquer outro, sem tratamento especial para ele. (até algum tempo depois :)
ZhongYu