Handling Nulls in nested objects (Java)
Handling NullPointerException and keeping track of all the nullable values has always been a pain for Java developers.
This is even worse when you are working with deeply nested objects and handling all the null values grows exponentially with each nullable nested value. This is visible by all the statements like:
if (x.y().z().. != null) {…}
In this article, we will explore some of the techniques starting with the naive approach to a more advanced one for handling such scenarios.So, are you ready? Let’s dive in…
Suppose we have a simple but nested object structure like:
root {
first-level {
second-level {
third-level {
name: "string"
}
}
}
}
where each object and/or value can be nullable. Say our objective is to create a greeting for the name nested three level deep behind root object.
Here are our test objects with our testing code:
Let’s start with the solutions:
Unsafe Approach:
This can result in a NullPointerException!
Result:
Failed to greet for r1 !
Failed to greet for r2 !
Failed to greet for r3 !
Greeting for r4 : Hello null
Greeting for r5 : Hello Lalit
Naive Safe Approach:
Result:
Greeting for r1 : Hey There!
Greeting for r2 : Hey There!
Greeting for r3 : Hey There!
Greeting for r4 : Hey There!
Greeting for r5 : Hello Lalit
Look at the indentation increasing with the increasing nesting level. Although, we could combine the null checks, but if we wanted to do other operation on each nested object then we would have to adopt the above approach.
The Optional Approach
Now, we will jump into the functional territory to solve this problem. Java 8’s Optional<T< comes to our rescue:
This works like a charm! The only problem is we cannot control and determine which stage failed or is null, and cannot have failover for each stage.
To implement that in Optional, we have to do like:
Result:
Greeting for r1 : Hello first level is null
Greeting for r2 : Hello second level null
Greeting for r3 : Hello third level is null
Greeting for r4 : Hey There!
Greeting for r5 : Hello Lalit
This will work, but we had to write a lot of orElse statements to handle a particular null case and we have to be careful as to where we want to put them.
What if there was a way to pass a default at each map stage…?
Custom Wrapper Approach
Let’s write our own NullableWrapper with mapper taking a default value at each map stage (We’ll consider null otherwise). Let’s call our object as NullableWrapper<T< which:
- Takes a supplier returning a value of type T (denoting lazy computation) or a value of type T.
- May take a default value of type T (if the supplier or value results in null) which is by default set to null.
- Can map the current wrapper<T< to new wrapper<R< by providing a transforming function Function<T,R< and may take a new default value of type R which is otherwise set to null.
- Can get the value by providing a new default value of type T which is otherwise set to null.
Now we have all the ingredients for the recipe, let’s start cooking!
Now, let’s taste it! I mean test it with the mapWithDefault feature (the other map will work exactly like Optional).
This will also work just like we expected, and the code looks more elegant.
One more benefit of this is that we don’t have to use the getOrElse every time as NullableWrapper’s .get() will return a default value or null (in case default not provided) instead of blowing up:
// will blow up!
Optional.ofNullable(null).map(x -> "Hello " + x).get();
// will return null
new NullableWrapper(() -< null).map(x -> "Hello " + x).get();
Now, let’s see the NullableWrapper«() from the point of view of a library user. It asks for a supplier or a value of type T and a default value.
This makes me think that the supplier evaluation must also be safe as it would be lazily evaluated inside the wrapper! Let’s try it out:
Result:
Uh oh! What happened there?
This means the .get() operation is not that safe as we thought! How to make it safe? Let’s try to fix this by gulping all the NPEs:
Now, we also want to do mapping by ignoring NPEs:
Let’s try that again:
Result:
Hey There!
Hi Lalit
Conclusion
We saw how we can utilise the power of Optionals and functional programming concepts to get the safety of the null checks as well as the control over the outcome with a default value.
Our own NullableWrapper gives following advantages over builtin Optional:
- Mapping with a default value at any stage.
- Defining initial default value.
- Super safe lazy computation by ignoring NPEs.
- Safer than NullableWrapper’s get() is more safer than Optional’s get() as it will return null or a default value as the final result and won’t just blow up.
Hope you liked it. Thanks for reading :)