¿Por qué findFirst () lanza una NullPointerException si el primer elemento que encuentra es nulo?

89

¿Por qué esto arroja 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

El primer artículo stringses null, que es un valor perfectamente aceptable. Además, findFirst()devuelve un Optional , que tiene aún más sentido para findFirst()poder manejar nulls.

EDITAR: actualizado orElse()para que sea menos ambiguo.

interminable
fuente
4
null no es un valor perfectamente aceptable ... use "" en su lugar
Michele Lacorte
1
@MicheleLacorte, aunque estoy usando Stringaquí, ¿qué pasa si es una lista que representa una columna en la base de datos? El valor de la primera fila de esa columna puede ser null.
neverendingqs
Sí, pero en java null no es aceptable .. use una consulta para establecer null en db
Michele Lacorte
10
@MicheleLacorte, nulles un valor perfectamente aceptable en Java, en términos generales. En particular, es un elemento válido para un ArrayList<String>. Sin embargo, como cualquier otro valor, existen limitaciones sobre lo que se puede hacer con él. "No lo use nunca null" no es un consejo útil, ya que no puede evitarlo.
John Bollinger
@NathanHughes: sospecho que para cuando llames findFirst(), no hay nada más que quieras hacer.
neverendingqs

Respuestas:

72

La razón de esto es el uso de Optional<T>en la devolución. Opcional no se permite contener null. Esencialmente, no ofrece ninguna forma de distinguir las situaciones "no está ahí" y "está ahí, pero está configurado para null".

Es por eso que la documentación prohíbe explícitamente la situación cuando nullse selecciona en findFirst():

Lanza:

NullPointerException - si el elemento seleccionado es null

Sergey Kalinichenko
fuente
3
Sería sencillo rastrear si existe un valor con un booleano privado dentro de instancias de Optional. De todos modos, creo que estoy al borde de despotricar: si el idioma no lo admite, no lo admite.
neverendingqs
2
@neverendingqs Absolutamente, usar a booleanpara diferenciar estas dos situaciones tendría mucho sentido. Para mí, parece que el uso de Optional<T>aquí fue una elección cuestionable.
Sergey Kalinichenko
1
@neverendingqs No puedo pensar en ninguna alternativa atractiva a esto, además de rodar su propio nulo , que tampoco es ideal.
Sergey Kalinichenko
1
Terminé escribiendo un método privado que obtiene el iterador de cualquier Iterabletipo, verifica hasNext()y devuelve el valor apropiado.
neverendingqs
1
Creo que tiene más sentido si findFirstdevuelve un valor opcional vacío en el caso de PO
danny
47

Como ya se comentó , los diseñadores de API no asumen que el desarrollador quiera tratar los nullvalores y los valores ausentes de la misma manera.

Si aún desea hacer eso, puede hacerlo explícitamente aplicando la secuencia

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

a la corriente. El resultado será un opcional vacío en ambos casos, si no hay primer elemento o si el primer elemento lo es null. Entonces, en su caso, puede usar

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

para obtener un nullvalor si el primer elemento está ausente o null.

Si desea distinguir entre estos casos, simplemente puede omitir el flatMappaso:

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

Esto no es muy diferente a su pregunta actualizada. Solo tienes que reemplazar "no such element"con "StringWhenListIsEmpty"y "first element is null"con null. Pero si no te gustan los condicionales, puedes lograrlo también como:

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

Ahora, firstStringserá nullsi un elemento existe pero existe nully será "StringWhenListIsEmpty"cuando no exista ningún elemento.

Holger
fuente
Lo siento, me di cuenta de que mi pregunta podría haber implicado que quería regresar nullpara 1) el primer elemento es nullo 2) no existen elementos dentro de la lista. He actualizado la pregunta para eliminar la ambigüedad.
neverendingqs
1
En el tercer fragmento de código, Optionalse puede asignar un null. Dado que Optionalse supone que es un "tipo de valor", nunca debe ser nulo. Y un Opcional nunca debe ser comparado por ==. El código puede fallar en Java 10 :) o siempre que se introduzca un tipo de valor en Java.
ZhongYu
1
@ bayou.io: la documentación no dice que las referencias a los tipos de valor no pueden ser nully aunque las instancias nunca deben ser comparadas ==, la referencia puede ser probada para su nulluso, ==ya que esa es la única forma de probarla null. No veo cómo nulldebería funcionar una transición de este tipo a "nunca " para el código existente, ya que incluso el valor predeterminado para todas las variables de instancia y elementos de matriz es null. El fragmento seguramente no es el mejor código, pero tampoco la tarea de tratar nulls como valores presentes.
Holger
ver john rose - ni se puede comparar con el operador "==", ni siquiera con un nulo
ZhongYu
1
Dado que este código utiliza una API genérica, esto es lo que el concepto llama una representación en caja que puede ser null. Sin embargo, dado que un cambio de lenguaje tan hipotético haría que el compilador emitiera un error aquí (no romper el código silenciosamente), puedo vivir con el hecho de que posiblemente tendría que ser adaptado para Java 10. Supongo que la StreamAPI también se ven bastante diferentes…
Holger
18

Puede utilizar java.util.Objects.nonNullpara filtrar la lista antes de buscar

algo como

list.stream().filter(Objects::nonNull).findFirst();
Mattos
fuente
1
Quiero firstStringserlo nullsi el primer elemento stringses null.
neverendingqs
2
desafortunadamente usa Optional.ofque no es seguro nulo. Usted podría mapa Optional.ofNullable y luego usar findFirst, pero el resultado final será con un opcional de opcional
Mattos
14

La información siguiente sustituye código findFirst()con limit(1)y reemplaza orElse()con reduce():

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

limit()permite que solo 1 elemento alcance reduce. El BinaryOperatorpasado a reducedevuelve ese elemento 1 o "StringWhenListIsEmpty"si ningún elemento alcanza el reduce.

La belleza de esta solución es que Optionalno está asignada y la BinaryOperatorlambda no va a asignar nada.

Nathan
fuente
1

Se supone que opcional es un tipo de "valor". (lea la letra pequeña en javadoc :) JVM podría incluso reemplazar todo Optional<Foo>con solo Foo, eliminando todos los costos de empaquetado y desembalaje. Un nullFoo significa un vacío Optional<Foo>.

Es un diseño posible permitir Opcional con valor nulo, sin agregar una bandera booleana; solo agregue un objeto centinela. (incluso podría usarse thiscomo centinela; ver la causa Throwable.)

La decisión de que Opcional no puede envolver nulos no se basa en el costo del tiempo de ejecución. Este fue un tema muy controvertido y necesita buscar en las listas de correo. La decisión no convence a todo el mundo.

En cualquier caso, dado que Optional no puede envolver un valor nulo, nos empuja a una esquina en casos como findFirst. Deben haber razonado que los valores nulos son muy raros (incluso se consideró que Stream debería prohibir los valores nulos), por lo tanto, es más conveniente lanzar una excepción en valores nulos en lugar de en streams vacíos.

Una solución es boxear null, por ejemplo

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

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

(Dicen que la solución a todos los problemas de programación orientada a objetos es introducir otro tipo :)

ZhongYu
fuente
1
No es necesario crear otro Boxtipo. El Optionaltipo en sí puede servir para este propósito. Vea mi respuesta como ejemplo.
Holger
@Holger: sí, pero eso puede ser confuso ya que no es el propósito previsto de Optional. En el caso de OP, nulles un valor válido como cualquier otro, sin tratamiento especial para él. (hasta algún tiempo después :)
ZhongYu