list.stream().map(Foo::getAttr)
... returns a stream with one element, with a value of null.
The JavaDoc for findAny() (and findFirst()) says:
Returns:
an Optional describing some element of this stream, or an
empty Optional if the stream is empty
Throws:
NullPointerException - if the element selected is null
So findAny() is doing exactly as documented: it's selecting a null, and as a result, throwing NullPointerException.
This makes sense because Optional is (again according to JavaDoc, but emphasis mine):
A container object which may or may not contain a non-null value
... which means you can guarantee that Optional.ifPresent( x -> x.method()) will never throw NullPointerException due to x being null.
So findAny() could not return Optional.of(null). And Optional.empty() means that the stream was empty, not that it found a null.
Many parts of the Stream/Optional infrastructure are about discouraging the use of nulls.
You can get around this by mapping the nulls to Optionals, to yield an Optional<Optional<Foo>> -- which looks a bit convoluted, but is an accurate representation of your domain. Optional.empty() means the stream was empty. Optional.of(Optional.empty()) means it found one null element:
list.stream().map(Foo::getAttr).map(Optional::ofNullable).findAny()