How Java Can Fix Its NPE ProblemWhen another language's compiler option may hold the keyApril 2, 2021
Photo by cottonbro from Pexels
I am currently obsessed with Java
Optional because one day, I accidentally strayed into its source code and got my mind blown. I knew OpenJDK was public, it's just that I never bothered to look into it until now. From there, I decided to study the code, what makes it tick, what makes
Optional so cool that it's being recommended everywhere.
Optional exists to avoid the dreaded
NullPointerException (which we'll call "NPE" going forward) - the error we get when we try to access a member of an instance that ended up being
null at runtime. Under the hood,
Optional just does null-checks. But it does this through a very elegant API. It provides utility methods, allows chaining, and propagates type information correctly so that we will always know what to expect.
But looking at the docs for
Optional, some APIs can actually NPE. It's throwing the very exceptions that we're trying to avoid! For example,
of NPEs when given a
null value. Or
map() NPEs if we provide a
flatMap() NPEs when the value is mapped to a
Well... not really.
To answer this weird behavior, we need to hop over to another language: TypeScript. In TypeScript, there's a compiler flag called
undefined are valid values to any type. This is the root of all NPEs because we often write code assuming the value is always there when they may not. But with
undefined become their own types. This means we can no longer assign
undefined to any type. And if we really have to, we have to explicitly do so, e.g. making a union type. Because of this, TypeScript can know which values can be
null and warn us at compile time for any misuse.
// Without --strictNullChecks const foo: string = 'Hello, World!' const bar: string = null // valid const baz: string = undefined // valid console.log(foo.length) console.log(bar.length) // EXPLOSION! console.log(bar.length) // EXPLOSION! // With --strictNullChecks const foo: string = 'Hello, World!' const bar: string = null // tsc will not compile const baz: string = undefined // tsc will not compile const qux: string | null = null const quux: string | undefined = undefined console.log(foo.length) console.log(qux.length) // tsc tells us to deal with the null case console.log(quux.length) // tsc tells us to deal with the undefined case
Back in Java, as far as I know, there is no equivalent for
--strictNullChecks. This means
null is still a valid value for any type to represent the absence of a value, e.g.
String foo = null;. This is why we need
Optional and, going back to the topic at hand, why Optional APIs throw NPEs from its APIs. That's because we're not just dealing with potentially nullable values here. The inputs of the various APIs can also be
OurBusinessObject defaultValue = new OurBusinessObject('nope'); OurBusinessObject nullableValueFromDb = null; Function<X, Y> dynamicMapper = null; OurBusinessObject finalValue = Optional .ofNullable<OurBusinessObject>(nullableValueFromDb) .map(v -> v.getRelatedBusinessObject()) // won't run because empty .map(dynamicMapper) // EXPLOSION! .orElse(defaultValue);
In the above example,
ourDynamicMapper can be
null because it's a valid value for
Function<X, Y>. From that point, it can wreak havoc in code because developers think it's a callable function without knowing it can be
null (and null-checking every single value is a pain). But over in TypeScript with
--strictNulLChecks, if we declared
dynamicMapper as a nullable value and implemented
map() to only accept a function, the compiler can determine ahead of time that
dynamicMapper cannot be a mapper for
const defaultValue: OurBusinessObject = new OurBusinessObject('nope') const nullableValueFromDb: OurBusinessObject | null = null const dynamicMapper: <X, Y> (v: X) => Y | null = null const finalValue = Optional .ofNullable<OurBusinessObject>(nullableValueFromDb) .map(v => v.getRelatedBusinessObject()) .map(dynamicMapper) // null cannot be assigned to (v: X) => Y .orElse(defaultValue)
If Java can implement something similar to
--strictNullChecks, that would be great. But with the stability requirements of the Java ecosystem and the amount of legacy code already out there, I doubt one can easily make such a big change. We can't rewrite all the Java code overnight to comply with this compiler option.
Optional might probably be the best solution for now.