java stream find first match
java 8 findFirst vs map on optional (4)
Dado este código:
class Foo {
Integer attr;
public Integer getAttr() {return attr;}
}
List<Foo> list = new ArrayList<>();
list.add(new Foo());
list.stream().map(Foo::getAttr).findAny().orElse(null); //A
list.stream().findAny().map(Foo::getAttr).orElse(null); //B
La línea A arroja
java.lang.NullPointerException: nulo
mientras que la línea B devuelve nulo.
¿Cuál es la causa de este comportamiento? Ambos findAny()
y map()
devuelven Optional<T>
.
En primer lugar, los dos fragmentos de código de código son diferentes operaciones:
// v--- stream intermediate operation
list.stream().map(Foo::getAttr).findAny().orElse(null); //A
// v---- a Optional utility method
list.stream().findAny().map(Foo::getAttr).orElse(null); //B
y la excepción NullPointerException
ocurre en Stream # findAny operation, ya que no puede aceptar un valor null
. debido a que usa Optional.of en vez de Optional.ofNullable . y la documentación de Stream # findAny ya afirma:
Lanza :
NullPointerException
: si el elemento seleccionado es nulo
así que si quiere que su fragmento de código A
funcione correctamente , debe filtrar todos null
valores null
antes de llamar a Stream # findAny , por ejemplo:
//when no elements in stream, `findAny` will be return a empty by Optional.empty()
// v
list.stream().map(Foo::getAttr).filter(Objects::nonNull).findAny().orElse(null);//A
list.stream().map(Foo::getAttr).findAny().orElse(null);
El documento Java para transmisiones dice que Stream: "devuelve una secuencia que consiste en los resultados de aplicar la función dada a los elementos de esta secuencia", y findAny () "podría devolver una excepciónNullPointerException, si el elemento seleccionado es nulo". En su clase Foo, entero (no int) por defecto se establece en nulo porque está declarado pero no inicializado. ver Primitivos ver valores predeterminados e inicialización de objetos en Java
La inicialización es diferente para: A) Miembros de clase (Objetos y primitivos) B) Variables locales
list.stream().map(Foo::getAttr)
... devuelve una secuencia con un elemento, con un valor de nulo.
El findAny()
para findAny()
(y findFirst()
) dice:
Devoluciones:
un Opcional que describe algún elemento de esta secuencia, o un Opcional vacío si la secuencia está vacía
Lanza:
NullPointerException: si el elemento seleccionado es nulo
Entonces findAny()
está haciendo exactamente lo que está documentado: está seleccionando un nulo y, como resultado, está lanzando NullPointerException
.
Esto tiene sentido porque Optional
es (de nuevo según JavaDoc, pero el énfasis es mío):
Un objeto contenedor que puede contener o no un valor no nulo
... lo que significa que puede garantizar que Optional.ifPresent( x -> x.method())
nunca lanzará NullPointerException
debido a que x
es nulo.
Entonces findAny()
no pudo devolver Optional.of(null)
. Y Optional.empty()
significa que la transmisión estaba vacía, no que haya encontrado un nulo.
Muchas partes de la infraestructura Stream
/ Optional
tratan de desalentar el uso de nulos.
Puede solucionar esto asignando los nulos a Optionals
, para obtener un Optional<Optional<Foo>>
, que se ve un poco enrevesado, pero es una representación precisa de su dominio. Optional.empty()
significa que la transmisión estaba vacía. Optional.of(Optional.empty())
significa que encontró un elemento nulo:
list.stream().map(Foo::getAttr).map(Optional::ofNullable).findAny()
Bueno, es obviamente debido al orden en el que se realizan estas operaciones y también porque findAny
explícitamente dice: throws NullPointerException if the element selected is null
Cuando hace un map(Foo::getAttr)
efectivamente ha mapeado eso a null
, entonces su Stream ahora contiene un null
; así findAny
rompe con una excepción (ya que findAny
se aplica en ese nulo)
La otra operación primero encuentra el objeto Foo
, luego lo mapea a Foo::getAttr
( Foo::getAttr
así a Optional.empty()
), por orElse
se llama a orElse
.
Además, esto tendría más sentido (al menos para mí):
list.stream()
.findAny()
.flatMap(f -> Optional.ofNullable(f.getAttr()))
.orElse(null);
flatMap
correlacionaría con Optional<Integer>
flatMap
Optional<Integer>
(atributos) Optional<Integer>
, en caso de que este esté empty
obtenga el resultado orElse
.