A bug was recently identified in a project I’m working on. Check the following code out:

private void addItem(Integer itemCode, Item itemFromDb){ if(itemCode == itemFromDb.getItemCode()){ //foo } else{ //bar } } 1 2 3 4 5 6 7 private void addItem ( Integer itemCode , Item itemFromDb ) { if ( itemCode == itemFromDb . getItemCode ( ) ) { //foo } else { //bar } }

Naughty naughty. I had done a very silly thing and used == to test equality of Integer objects! As we all know, when comparing equality on objects we should be using .equals(). However, this bug was intermittent. Comparing Integer to Integer like this works. Sometimes.

Let’s see what’s happening here:

First, we need to check what happens when we compare two ints:

public static void main(String[] args) { compareInts(); } private static void compareInts(){ int a = 10; int b = 10; checkUnboxed(a, b); } private static void checkUnboxed(int a, int b){ System.out.println("Comparing unboxed " + a + " to " + b); System.out.println(a == b); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public static void main ( String [ ] args ) { compareInts ( ) ; } private static void compareInts ( ) { int a = 10 ; int b = 10 ; checkUnboxed ( a , b ) ; } private static void checkUnboxed ( int a , int b ) { System . out . println ( "Comparing unboxed " + a + " to " + b ) ; System . out . println ( a == b ) ; }

Output:

Comparing unboxed 10 to 10

true

This makes sense. A is of an equal value to B.

Now let’s compare two Integer objects:

public static void main(String[] args) { compareIntegers(); } public static void compareIntegers() { Integer first = 10; Integer second = 10; checkBoxed(first, second); } private static void checkBoxed(Integer a, Integer b) { System.out.println("Comparing boxed " + a.toString() + " to "+ b.toString()); System.out.println(a == b); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public static void main ( String [ ] args ) { compareIntegers ( ) ; } public static void compareIntegers ( ) { Integer first = 10 ; Integer second = 10 ; checkBoxed ( first , second ) ; } private static void checkBoxed ( Integer a , Integer b ) { System . out . println ( "Comparing boxed " + a . toString ( ) + " to " + b . toString ( ) ) ; System . out . println ( a == b ) ; }

Output:

Comparing boxed 10 to 10

true

Great – we have equal values, right? Where’s the problem?

Here:

Integer first = 10; Integer second = 10; 1 2 Integer first = 10 ; Integer second = 10 ;

Change the values to some high number. Let’s say 1000:

Comparing boxed 1000 to 1000

false

Depending on your particular implementation of Java, the value required to trigger this behaviour may vary, but in my case, the equality result changes when a and b are set to 128.

UPDATE: As pointed out by Edwin Nathaniel in the comments, any Java implementation should cache Integer objects according to the following:

“Cache to support the object identity semantics of autoboxing for values between -128 and 127 (inclusive) as required by JLS.

The cache is initialized on first usage. The size of the cache may be controlled by the -XX:AutoBoxCacheMax= option.”

This means that the Integer cache limits are configuration dependent, not implementation dependent. Props to Edwin (who also has a rather delicious looking blog) for schooling me on this subject!

Let’s change the code a little and turn our primitives into objects when we do the comparison (this is what’s already happening, by the way, it’s just a little less obvious the way I’ve illustrated it here):

public static void main(String[] args) { compareInts(); } public static void compareInts() { int a = 1000; int b = 1000; checkBoxed(a, b); } private static void checkBoxed(Integer a, Integer b) { System.out.println("Comparing boxed " + a.toString() + " to "+ b.toString()); System.out.println(a == b); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public static void main ( String [ ] args ) { compareInts ( ) ; } public static void compareInts ( ) { int a = 1000 ; int b = 1000 ; checkBoxed ( a , b ) ; } private static void checkBoxed ( Integer a , Integer b ) { System . out . println ( "Comparing boxed " + a . toString ( ) + " to " + b . toString ( ) ) ; System . out . println ( a == b ) ; }

Output:

Comparing boxed 1000 to 1000

false

In case you missed it, the checkBoxed method is expecting Integer objects, not primitives. We pass it primitives, and autoboxing is applied by the compiler, cramming our primitives into objects and giving us a nasty surprise.

Autoboxing allows developers to forget about the tedium of converting between primitives and objects, resulting in tidier code and, in this particular case, more headaches. You don’t gain anything performance-wise as the compiler will still generate the ‘boxing’ code, but it does tend to make life more convenient.

If you want to avoid this problem, you can do the following:

Integer first = new Integer(10); Integer second = new Integer(10); checkBoxed(a, b); 1 2 3 Integer first = new Integer ( 10 ) ; Integer second = new Integer ( 10 ) ; checkBoxed ( a , b ) ;

Comparing these two using the checkBoxed method will always output ‘false’, because you are explicitly creating two different objects. The fact that they represent the same value is irrelevant to the JVM. It sees two different objects and tells you so.

You can also use the following comparison instead:

Integer a = 1000; Integer b = 1000; System.out.println(a.intValue() == b.intValue()); 1 2 3 Integer a = 1000 ; Integer b = 1000 ; System . out . println ( a . intValue ( ) == b . intValue ( ) ) ;

Perhaps the best way to avoid this problem is to be consistent and enforce the rule across your team. Keep in mind that you don’t always know how Bill has implemented his bit of code, and he doesn’t know how you’ve implemented yours – so decide how you’re going to deal with this potential pitfall and make sure you both keep it in mind. Unfortunately for me, I’m the only person working on this project so I can’t blame somebody else.

It’s worth pointing out that autoboxing is applied to all primitive wrapper types, not just Integers, so it is an important feature to keep in mind.

If you use Eclipse, turn on the following handy feature to help you find where autoboxing / unboxing could occur:

This will highlight your code with the following warning:

This is one of those features added to make your life easier. Unfortunately, people new to Java, or idiots like me, may not understand what is happening here and could be writing code laden with boxing issues. Only a good set of coding standards, and a decent code-review process, will help you catch these issues before they become a real problem out in the field.