How to make java 8 Stream map continuously with null check

Viewed 19017

I have this piece of code

Coverage mainCoverage = illus.getLifes().stream()
    .filter(Life::isIsmain)
    .findFirst()
    .orElseThrow(() -> new ServiceInvalidAgurmentGeneraliException(env.getProperty("MSG_002")))
    .getCoverages()  
    .stream() // <==may cause null here if list coverage is null
    .filter(Coverage::isMainplan)
    .findFirst()
    .orElseThrow(() -> new ServiceInvalidAgurmentGeneraliException(env.getProperty("MSG_002")));

which is totally work fine but I think It's a little bit messy and not cover all the null pointer exception possible (see the comment).

I try to refactor this code into

Coverage mainCoverage1 = illus.getLifes().stream()
    .filter(Life::isIsmain)
    .map(Life::getCoverages)
    .filter(Coverage::isMainplan) //<== cannot filter from list coverage to one main coverage
    ...

Seem after I map the life to coverage it is not a list of coverage anymore. So the question is how I can refactor the first section into null safe and maybe shorten it?

5 Answers

Life::getCoverages returns a collection hence the filter Coverage::isMainplan will not work, instead you should flatMap the sequences returned after .map(Life::getCoverages) then apply the filter operation on the Coverage:

Coverage mainCoverage = 
          illus.getLifes()
               .stream()
               .filter(Life::isIsmain)               
               .map(Life::getCoverages)
               //.filter(Objects::nonNull) uncomment if there can be null lists
               .flatMap(Collection::stream) // <--- collapse the nested sequences
               //.filter(Objects::nonNull) // uncomment if there can be null Coverage
               .filter(Coverage::isMainplan)
               .findFirst().orElse(...);

I've added a few things to your code:

  1. I've added .filter(Objects::nonNull) after .map(Life::getCoverages) which you can uncomment given the elements returned could potentially be null.
  2. I've added .flatMap(Collection::stream) which returns a stream consisting of the results of replacing each element of this stream with the contents of a mapped stream produced by applying the provided mapping function to each element.
  3. I've added another .filter(Objects::nonNull) which you can uncomment given the elements returned after flatMap could potentially be null.
  4. We're then at a stage in which we can apply .filter(Coverage::isMainplan) and finally, retrieve the first object meeting the criteria via findFirst and if none then provide a default value via orElse.

I'd suggest having a look at the following blogs to get familiar with the flatMap method:

Add condition in filter if list is not null and i.isIsmain then only filter, you can use public static boolean isNull(Object obj) or public static boolean nonNull(Object obj)

Coverage mainCoverage = illus.getLifes().stream()
.filter(i->i.isIsmain && Objects.nonNull(i.getCoverages()))
.findFirst()
.orElseThrow(() -> new ServiceInvalidAgurmentGeneraliException(env.getProperty("MSG_002")))
.getCoverages()  
.stream() // <==may cause null here if list coverage is null
.filter(Coverage::isMainplan)
.findFirst()
.orElseThrow(() -> new ServiceInvalidAgurmentGeneraliException(env.getProperty("MSG_002")));

In the first part of your code you can put in a filter(e -> e != null) to unsure if the List is null it wont throw an NPE:

Coverage mainCoverage = illus.getLifes().stream()
         .filter(Life::isIsmain)
         .findFirst()
         .orElseThrow(() -> new ServiceInvalidAgurmentGeneraliException(env.getProperty("MSG_002")))
         .getCoverages()  
         .filter(e -> e != null) //<=== Filter out all null values
         .stream()
         .filter(Coverage::isMainplan)
         .findFirst()
         .orElseThrow(() -> new ServiceInvalidAgurmentGeneraliException(env.getProperty("MSG_002"))

The problem with your second code snippet is that I'm assuming Life::getCoverages returns a Collection, not individual Coverage objects, so you cannot call Coverage::isMainplan on it

The example you gave and the answers given here for it, break some principles of clean functional style coding.

Firstly, you should not mix terminal actions and creating streams again in the same chain/pipeline of code. Like in your example stream().findFirst().orElseThrow(..).stream().otherActions. It's really not a good practice and error prone. Ideally a chain of Java Stream API calls should work with a single Stream. That way it's easier to follow and reason about your code.

Secondly, you mentioned the this bit may hit Null Pointer exc:

.getCoverages()  
.stream() // <==may cause null here if list coverage is null

if getCoverages() is supposed to return collection, it should never return null, always return empty collection instead.

Break your code into separate logical pieces and give it some meaningful names:

Coverage mainCoverage = illus.getLifes().stream()
    .filter(Life::isIsmain)
    .findFirst()
    .orElseThrow(() -> new ServiceInvalidAgurmentGeneraliException(env.getProperty("MSG_002")));

Coverage mainplan = mainCoverage.getCoverages().stream()
    .filter(Coverage::isMainplan)
    .findFirst()
    .orElseThrow(() -> new ServiceInvalidAgurmentGeneraliException(env.getProperty("MSG_002")));

It looks much better than your starting point. I hope this helps.

You can try to encapsulate the resulting Collection<Coverage> into an Optional<Collection<Coverage>> so that you can map over in a null safe manner.

final Supplier<ServiceInvalidAgurmentGeneraliException> customExceptionThrower = () -> new ServiceInvalidAgurmentGeneraliException(env.getProperty("MSG_002"));

final Collection<Coverage> firstMainLifeCoverages = illus.getLifes().stream()
    .filter(Life::isIsmain)
    .findFirst()
    .orElseThrow(customExceptionThrower)
    .getCoverages();

Optional.ofNullable(firstMainLifeCoverages)
    .map(Collection::stream)
    .orElseThrow(customExceptionThrower)
    .filter(Coverage::isMainplan)
    .findFirst()
    .orElseThrow(customExceptionThrower);
Related