Java Unboxing Pitfall
NullPointerException when returning
This is just a short article about one of the pitfalls you might encounter with Java's magical
Autoboxing/Unboxing feature. This lovely feature came about with Java 5 (Tiger) many moons ago, allowing an
almost interchangeability between primitives and their class counterparts, such as int
and
Integer
. We call primitives or types that have corresponding wrappers, "Boxed Types".
Autoboxing is a language feature that allows for automatic wrapping of primitives in their respective
wrapper classes.
Eg: Float f = 2.0f;
Unboxing is a language feature that allows for extracting a primitive value from the corresponding
wrapper class.
Eg: int f = new Integer(2.0f);
Having boxing allows us to do programming with generics and collections and all the fun stuff, so it was a great feature addition to Java.
NullPointerException on return
My first experience of this pitfall went something like this. A colleague was investigating an
NPE
that occurred in production. Interested, I looked at the stack trace with him, which was of the form..
Exception in thread "main" java.lang.NullPointerException
at com.andrew_flower.example.MagicalFoxService.countFoxes(MagicalFoxService.java:25)
...
at com.andrew_flower.example.ClassOfAwesome.doAmazingNow(ClassOfAwesome.java:12)
at com.andrew_flower.example.Main.main(Main.java:10)
We followed the stack trace to the offending line, only to find a simple return statement like that below
on line 25. We saw no dot (.
) - the usual culprit at the seen of an NPE - just a
return
statement.
public int countFoxes() {
Integer foxCount = countThemBackwards();
....
return foxCount;
}
Our first thought was that we were looking at the wrong version of the code, and only a few minutes later
did we notice the method return type, variable type. (It looks more obvious in this reduced example, I
promise!)
And the obvious logic that led to the possibility of it
being null
when returning. The above code shows a pattern that permits this common pitfall.
It is probably not a good idea to write code like this, for reasons just like this. Autoboxing and Unboxing
are great, but as with all programming constructs, should be used carefully.
Reproducing the bug
If we want to reproduce this to see how it works, we can construct a contrived example like below
package com.andrew_flower.demo;
public class Main {
public static int getInteger(final Integer example) {
return example;
}
public static void main(String[] args) {
System.out.println(getInteger(null));
}
}
Here getInteger
will try it's best to convert the Integer
reference to nothing
into
a primitive integer value. And it will fail, and you will cry.
How does Unboxing work?
It doesn't take much intuition to understand that null
cannot be converted to an integer value.
But do you know how the Java Runtime is actually trying to do the conversion? If we take the contrived
example
above and we look at the compiled output, we can see.
PROTIP: If you ever need to view the bytecode for a class, you can use the javap
command
that comes
with any Java SDK. javap
is a Java Disassembler, and will disassemble a compiled class file
according to the parameters you give. Below is how you can output the bytecode, using the -c
flag.
javap -c com.andrew_flower.demo.Main
#OR
javap -c com/andrew_flower/demo/Main.class
Doing that on our example above produces the following bytecode
Compiled from "Main.java"
public class com.andrew_flower.demo.Main {
public com.andrew_flower.demo.Main();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static int getInteger(java.lang.Integer);
Code:
0: aload_0
1: invokevirtual #2 // Method java/lang/Integer.intValue:()I
4: ireturn
public static void main(java.lang.String[]);
Code:
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
3: aconst_null
4: invokestatic #4 // Method getInteger:(Ljava/lang/Integer;)I
7: invokevirtual #5 // Method java/io/PrintStream.println:(I)V
10: return
}
If we look at the getInteger
method
we can see the behaviour, firstly aload_0
is called, which loads the first reference argument
and
pushes it onto the stack - which in this case (because it is a static method) is the Integer
reference
as defined in the method parameter list and passed in from main
. In our case this is always
null
. Following that invokevirtual #2
is executed. #2
is a reference
to a constant in the constant pool. If you run javap
with the -verbose
flag, it
will show the constant pool, but javap
is very kind and gives us comments explaining which
methods are invoked anyway, so we don't need that.
So that is the magical step on line 12. That is how unboxing is done in bytecode for
Integer
s:
the intValue()
method is called to produce a primitive int
. But if the object is
null
ourInteger.intValue()
will result in an NPE.
If you're interested, why not see how the reverse is done - how does a primitive get autoboxed into a
wrapper class, like Integer a = 5
.
Summary
These are some take aways from this short and simple post:
- Autoboxing and Unboxing is a cool feature
- Autoboxing and Unboxing should still be used carefully
- Unboxing of integers is done by calling
intValue()
of theInteger
- Autoboxing of integers is done by passing them to the static
Integer.valueOf()
method javap
is a useful command for disassembling class files into readable bytecode
How to prevent this pitfall from happening:
- Don't rely on unboxing in a return statement
- Like dereferencing pointers in C, or using a dot "." in Java, always consider the possibility that a boxed
instance might be
null
- Write tests!
References
Bitcoin
Zap me some sats
This blog post itself is licensed under a Creative Commons Attribution 4.0 International License. However, the code itself may be adapted and used freely.