Helpful NullPointerExceptions in Java

August 26, 2019

Introduction

NullPointerExceptions are often a nightmare to deal with in Java. Apart from the fact that they sneak around, they are also difficult to debug.

The logs generated by NPEs are as abstract as you can get.

JEP-358 plans to offer more readability to NPEs.

Problem

In the following line, what is generating the NPE :

String ancestorsName = person.getParent().getParent().getName();

Impossible to say without debugging since the message will be in the likes of :

Exception in thread "main" java.lang.NullPointerException
    at Prog.main(Prog.java:5)

Another tricky one is :

List<Integer> ints = ...
printInt(ints.get(0));

[...]

public void printInt(int input) { ... }

How to know for sure whether List is null or its first index?

These are some examples of situations the JEP will solve.

Solution

The solution is to build the exception message based on what bytecode operations are being used at the moment of the unpleasant NPE.

Multiple ones are possible, but let’s take both examples from above and see how it breaks down.

Example 1

The line :

String ancestorsName = person.getParent().getParent().getName();

When compiled, gives the following bytecode :

20: aload_1
21: invokevirtual #6 [..] Person.getParent:()LMain$Person;
24: invokevirtual #6 [..] Person.getParent:()LMain$Person;
27: invokevirtual #7 [..] Person.getName:()Ljava/lang/String;
30: astore_2

Now, if we base ourselves on the following table from the JEP, we can define one single type of error possible (*astore *and *aload *are not possible here, it only applies to array objects)

Error Mappings From Bytecode to Exception MessageError Mappings From Bytecode to Exception Message

We can thus only be facing an exception with an explanation message Cannot invoke ’’ , this is already great. But in this case, it doesn’t really help and we could have imagined it ourselves.

Examples exist where the message can be really meaningful :

a.getStrings()[1] = b.c[2].getPerson().getName();

For more classical examples, the exception message also gives the exact variable throwing the exception. In our example, the exception message would look like this :

Exception in thread "main" java.lang.NullPointerException:
    Cannot invoke 'getParent()' because 'person' is null.

Or even

Exception in thread "main" java.lang.NullPointerException:
    Cannot invoke 'getName()' because 'person.getParent().getParent()' is null.

We can directly determine, from the logs, where exactly to concentrate the investigation, which will certainly save a lot of useless debugging.

Example 2

Our example with the int ‘s unboxing gives us the following compiled

51: aload_3
52: iconst_0
53: invokeinterface #11, java/util/List.get:(I)Ljava/lang/Object;
58: checkcast     #12    class java/lang/Integer
61: invokevirtual #13    Method java/lang/Integer.intValue:()I
64: invokestatic  #14    Method printInt:(I)V

The bytecode is modified to keep only the interesting parts of course.

Here, we can clearly see two invocations risking to throw a NPE :

  • invokeinterface for List#get
  • invokevirtual for Integer#intValue

For each of these, a specific message will be built.

Exception in thread "main" java.lang.NullPointerException:
    Cannot invoke 'get(int)' because 'ints' is null.

And for the second one, a very specific one too

Exception in thread "main" java.lang.NullPointerException:
    Cannot invoke 'intValue()' because 'ints.get(0)' is null.

Conclusion

While the messages presented in the article are mostly constructed manually and based on assumptions, you can already take them as an example of how the language will evolve with NPE.

This extension of the NPE’s messages will help save lots of precious minutes in debugging production code where debugging possibilities are limited.

No more “safe” design decisions anymore and more readable and compact code is the goal. Designing classes solely based on NPEs risks will not anymore be a pain.