andrew Flower

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 Integers: 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 the Integer
  • 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


Donate

Bitcoin

Zap me some sats

THANK YOU

Creative Commons License
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.