javaoptional-chaining

Is it possible to LOG different messages based on which step null is encountered while doing nested null check with Optional and map


I have a nested object which can return a null at any point of time.

Thanks to Optional and map we can now do nested calls without having to put null checks after every get.

I have a very unique requirement where I need to know at which step exactly did I encounter a null object for e.g. (Copied from StackOverflow)

Optional.of(new Outer())
  .map(Outer::getNested)
  .map(Nested::getInner)
  .map(Inner::getFoo)
  .ifPresent(System.out::println);

How can I LOG a different kind of log message depending on when and where I encounter a null value?

The code below is not valid but I am just trying to explain how it might look like programmatically:

Optional.of(outerObject).else(LOG.error("Outer was null"))
  .map(Outer::getNested).else(LOG.error("Nested was null"))
  .map(Nested::getInner).else(LOG.error("Inner was null"))
  .map(Inner::getFoo).else(LOG.error("Foo was null"))
  .ifPresent(System.out::println);

Solution

  • If this is a one-off thing, I would write a helper method that "wraps" the method references. The wrapper would return what the wrapped function returns, but if the wrapped method returns null, it also logs a message.

    private static <T, R> Function<T, R> withNullMessage(Function<? super T, ? extends R> function, String message) {
        return t -> {
            R r = function.apply(t);
            if (r == null) {
                Log.error(message);
            }
            return r;
        };
    }
    
    Optional.of(foo)
            .map(withNullMessage(Foo::getBar, "Bar is null!"))
            .map(withNullMessage(Bar::getBaz, "Baz is null!"))
            ...
    

    Note that this does not handle the case where foo is null. If foo is null, this will throw an exception. To handle this, you can start with a definitely-not-null thing,

    Optional.of("")
            .map(withNullMessage(x -> foo, "Foo is null!"))
            .map(withNullMessage(Foo::getBar, "Bar is null!"))
            .map(withNullMessage(Bar::getBaz, "Baz is null!"))
    

    Or you can write your own of that logs nulls.

    Another drawback of this is that it doesn't work with flatMaps. e.g. this does not work as you'd expect:

    .flatMap(withNullMessage(Foo::thisReturnsAnotherOptional, "..."))
    

    You would need another wrapper method to handle that case.

    If you need this sort of thing a lot, it's probably worth it to write your own Optional-like type, whose map methods take an extra argument.