0

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);
Nick Div
  • 5,338
  • 12
  • 65
  • 127

3 Answers3

1

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) {
}
ODDminus1
  • 544
  • 1
  • 3
  • 11
  • 1
    You should define your own exception class -- otherwise *all* `RuntimeException`s thrown in that try block will be reported as a null! – tgdavies Jul 20 '23 at 06:55
  • That's a good point. I will update my answer. – ODDminus1 Jul 20 '23 at 07:07
  • @ODDminus1 Thank you for the answer, really appreciate it. This looks like it will work but I also feel like I am going back to old ways of handling Null Pointers which I guess is the only way in my situation. – Nick Div Jul 20 '23 at 07:16
1

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.

Sweeper
  • 213,210
  • 22
  • 193
  • 313
  • This is very helpful. Thanks a lot. Instead of using Optional.of, if I use Optional.ofNullable(withNullMessage(() -> foo, "Foo is Null")) - would that work? – Nick Div Jul 20 '23 at 07:35
  • @NickDiv No, that's not what `ofNullable` does. You could do make an optional out of the wrapper, and then `apply`, `Optional.of(withNullMessage(x -> foo, "Foo is Null")).map(x -> x.apply(null))`. But I think that is not any better... – Sweeper Jul 20 '23 at 07:39
  • Makes sense. When I tried running it, i understood it better. Thanks for the answer – Nick Div Jul 20 '23 at 07:54
1

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;
    }
Sagar
  • 104
  • 1
  • 5