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);
3

There are 3 best solutions below

3
Sweeper On BEST ANSWER

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.

3
ODDminus1 On

You can achieve the required behaviour with some exception handling like this:

public class Test {
    public static void main(String[] args) {
        Outer outer = new Outer(new Nested(new Inner("value")));
//        Outer outer = new Outer(new Nested(new Inner(null)));
//        Outer outer = new Outer(new Nested(null));
//        Outer outer = new Outer(null);
//        Outer outer = null;
        try {
            Optional.ofNullable(outer).or(() -> throwEx("Outer was null"))
                    .map(Outer::nested).or(() -> throwEx("Nested was null"))
                    .map(Nested::inner).or(() -> throwEx("Inner was null"))
                    .map(Inner::foo).or(() -> throwEx("Foo was null"))
                    .ifPresent(System.out::println);
        } catch (NullValueException e) {
            System.out.println(e.getMessage());
        }
    }

    private static <T> T throwEx(String msg) {
        throw new NullValueException(msg);
    }
}

class NullValueException extends RuntimeException {
    public NullValueException(String msg) {
        super(msg);
    }
}

record Outer(Nested nested) {
}

record Nested(Inner inner) {
}

record Inner(String foo) {
}
0
Sagar On

Following the algorithm of one-off error mentioned in previous Sweeper's answer we could implement as below.This will take care of corner null cases as well

    public static void clientMethod() {
        Outer validOuter = new Outer(new Nested(new Inner("s")));
        Outer nullNested = new Outer(new Nested(null));
        Optional.ofNullable(nullNested)
                .map(outer->eam(outer, Function.identity(),"Extracted value: Outer was null"))
                .map(outer ->eam(outer, Outer::getNested,"Extracted value:Nested was null"))
                .map(nested->eam(nested, Nested::getInner,"Extracted value:Inner was null"))
                .map(inner->eam(inner, Inner::getFoo,"Extracted value:Foo was null"))
                .ifPresent(System.out::println);
    }

    public static <T,R> R eam (T object,Function<T,R> extracter,String extractedValueNullMessage){
        if(object != null){
            R extractedValue = extracter.apply(object);
            if (extractedValue == null){
                LOG.error(extractedValueNullMessage);
            }
            return extractedValue;
        }
        return null;
    }