简体   繁体   中英

Nested null checks in Java8 Optional vs Stream

This is a question regarding a possibly confusing Streams behavior. I came under the impression that the .map operaion (because of it's usage in Optional) was always null-safe (I'm aware that these .map 's are different implementations thought they share the same name). And I was quite surprised when I got a NPE when I used it so in a (list) stream. Since then, I started using Objects::nonNull with streams (both with .map & .flatMap operations).

Q1. Why is it that Optional can handle nulls at any level, whereas Streams can't (at any level), as shown in my test code below? If this is the sensible and desirable behavior, please give an explanation (as to it's benefits, or the downsides of List Stream behaving like Optional).

Q2. As a follow up, is there an alternative to the excessive null-checks that I perform in the getValues method below (which is what prompted me to think why Streams could not behave like Optional).

In the below test, I'm interested in the innermost class's value field only.

I use Optional in getValue method.

I use Streams on list in getValues method. And I cannot remove a single nonNull check in this case.

import lombok.AllArgsConstructor;
import lombok.Getter;

import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;

public class NestedObjectsStreamTest {
    @Getter @AllArgsConstructor
    private static class A {
        private B b;
    }
    @Getter @AllArgsConstructor
    private static class B {
        private C c;
    }
    @Getter @AllArgsConstructor
    private static class C {
        private D d;
    }
    @Getter @AllArgsConstructor
    private static class D {
        private String value;
    }

    public static void main(String[] args) {
        A a0 = new A(new B(new C(new D("a0"))));
        A a1 = new A(new B(new C(new D("a1"))));
        A a2 = new A(new B(new C(new D(null))));
        A a3 = new A(new B(new C(null)));
        A a5 = new A(new B(null));
        A a6 = new A(null);
        A a7 = null;

        System.out.println("getValue(a0) = " + getValue(a0));
        System.out.println("getValue(a1) = " + getValue(a1));
        System.out.println("getValue(a2) = " + getValue(a2));
        System.out.println("getValue(a3) = " + getValue(a3));
        System.out.println("getValue(a5) = " + getValue(a5));
        System.out.println("getValue(a6) = " + getValue(a6));
        System.out.println("getValue(a7) = " + getValue(a7));

        List<A> aList = Arrays.asList(a0, a1, a2, a3, a5, a6, a7);

        System.out.println("getValues(aList) " + getValues(aList));
    }

    private static String getValue(final A a) {
        return Optional.ofNullable(a)
            .map(A::getB)
            .map(B::getC)
            .map(C::getD)
            .map(D::getValue)
            .orElse("default");
    }

    private static List<String> getValues(final List<A> aList) {
        return aList.stream()
            .filter(Objects::nonNull)
            .map(A::getB)
            .filter(Objects::nonNull)
            .map(B::getC)
            .filter(Objects::nonNull)
            .map(C::getD)
            .filter(Objects::nonNull)
            .map(D::getValue)
            .filter(Objects::nonNull)
            .collect(Collectors.toList());
    }
}

Output

getValue(a0) = a0
getValue(a1) = a1
getValue(a2) = default
getValue(a3) = default
getValue(a5) = default
getValue(a6) = default
getValue(a7) = default

getValues(aList) [a0, a1]

Q1. Why is it that Optional can handle nulls at any level, whereas Streams can't (at any level), as shown in my test code below?

A Stream can "contain" null values. An Optional can't, by contract (the contract is explained in the javadoc): either it's empty and map returns empty, or it's not empty and is then guaranteed to have a non-null value.

Q2. As a follow up, is there an alternative to the excessive null-checks that I perform in the getValues method below.

Favor designs which avoid using nulls all over the place.

Q1. Why is it that Optional can handle nulls at any level, whereas Streams can't (at any level), as shown in my test code below?

Optional was created to handle the null values on itself, ie where the programmer did not want to handle the Nulls by himself. The Optional.map() method converts the value to an Optional object. Thus handling the nulls and taking away the responsibility from the developers.

Streams , on the other hand, leaves the handling of nulls on the developers, ie what if a developer might want to handle the nulls in a different way. Look at this link . It provides different choices the developers of the Stream had and their way of reasoning on each of the cases.

Q2. As a follow-up, is there an alternative to the excessive null-checks that I perform in the getValues method below

In cases of uses like you mentioned, where you don't want to handle the null cases, go with the Optional . As @JB Nizet said, avoid null scenarios in case of using streams or handle it yourself. This argues similarly. If you go through the first link I shared, you would probably get that Banning Null from a stream would be too harsh, and Absorbing Null would hamper the truthfulness of size() method and other functionalities.

Here are the codes you can try:

aList.stream()
    .map(applyIfNotNull(A::getB))
    .map(applyIfNotNull(B::getC))
    .map(applyIfNotNull(C::getD))
    .map(applyIfNotNullOrDefault(D::getValue, "default"))
    .filter(Objects::nonNull)
    .forEach(System.out::println);

With the below utility methods:

public static <T, U> Function<T, U> applyIfNotNull(Function<T, U> mapper) {
  return t -> t != null ? mapper.apply(t) : null;
}

public static <T, U> Function<T, U> applyIfNotNullOrDefault(Function<T, U> mapper, U defaultValue) {
  return t -> t != null ? mapper.apply(t) : defaultValue;
}

public static <T, U> Function<T, U> applyIfNotNullOrElseGet(Function<T, U> mapper, Supplier<U> supplier) {
  return t -> t != null ? mapper.apply(t) : supplier.get();
}

Not sure how it looks to you. but I personally don't like map(...).map(...)... . Here what I like more:

aList.stream()
    .map(applyIfNotNull(A::getB, B::getC, C::getD))
    .map(applyIfNotNullOrDefault(D::getValue, "default"))
    .filter(Objects::nonNull)
    .forEach(System.out::println);

With One more utility method:

public static <T1, T2, T3, R> Function<T1, R> applyIfNotNull(Function<T1, T2> mapper1, Function<T2, T3> mapper2,
    Function<T3, R> mapper3) {
  return t -> {
    if (t == null) {
      return null;
    } else {
      T2 t2 = mapper1.apply(t);
      if (t2 == null) {
        return null;
      } else {
        T3 t3 = mapper2.apply(t2);
        return t3 == null ? null : mapper3.apply(t3);
      }
    }
  };
}

Your Q1 has already been answered by raviiii1 and JB Nizet. Regarding your Q2:

is there an alternative to the excessive null-checks that I perform in the getValues method below

You could always combine both Stream and Optional like this:

private static List<String> getValues(final List<A> aList) {
    return aList.stream()
            .map(Optional::ofNullable)
            .map(opta -> opta.map(A::getB))
            .map(optb -> optb.map(B::getC))
            .map(optc -> optc.map(C::getD))
            .map(optd -> optd.map(D::getValue))
            .map(optv -> optv.orElse("default"))
            .collect(Collectors.toList());
}

Of course, this would be much cleaner:

private static List<String> getValues(final List<A> aList) {
    return aList.stream()
            .map(NestedObjectsStreamTest::getValue)
            .collect(Collectors.toList());
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM