Autoboxing Foils Me Again
Autoboxing is a Java helper introduced with the JDK2v1.5 release that allows some common transformations to be handled by the compiler instead of forcing developers to add implicit code.
A trivial example of this is transforming between the Integer object and the int primitive. A pre-autoboxing example would require us to write code like this if we wanted to prepare an Integer from an int value:
Integer legalDrinkingAge = Integer.valueOf(21);
Now that we have autoboxing, we can simply write code like this and Java will straighten it out for us:
Integer legalCarRentalAge = 25;
It makes for some much neater code in a lot of cases. Also allows for a lot less code in almost every case.
It has an unfortunate side effect, however. This is because a legal value for an Integer is "null" indicating that the value has not been created. There is no equivalent for an int. It is not possible to have an undefined primitive. Some may argue that zero would be a fine default, but zero is, in fact, a value. Say we were looking at checkbook balances; zero would indicate that a checking account was out of money, while a null value would instead indicate there was no checking account.
The side effect that I've encountered comes in a Java Bean (for this discussion, a trivial Object that has properties and simple methods like setters and getters). The bean worked fine until someone tried to stick a null Integer into the int field. Here's a super-simple bean that has a single primitive value we want to set.
public class Foo { private int foo = 0; public void setFoo(int foo) { this.foo = foo; } public int getFoo() { return foo; } }
The following lines illustrate the problem we have:
Integer five = new Integer(5); Foo foo = new Foo(); foo.setFoo(5); foo.setFoo(five); five=null; foo.setFoo(five);
The first setFoo() line uses the primitive int value of 5, and it works just dandy. The second setFoo() line uses the Integer defined above it, which also has a value of 5, and autoboxing saves the day by correctly converting the Integer to an int. However, the last setFoo() happens after we tell our program to forget the value of "five," and it tries to call setFoo() with a null value. This will compile, but it will fail to execute and run-time exceptions will be thrown.
A simple fix is to change the bean to instead hold an Integer. Simple enough, and this is the wigginess that just bit me.
public class Foo { private Integer foo = null; public void setFoo(Integer foo) { this.foo = foo; } public Integer getFoo() { return foo; } }
Now the same set of code above will work just fine. This time, instead autoboxing will conveniently turn the first setFoo() into an Integer in the nick of time, and the other two will just replace the current value our instance of Foo is holding.
The bug comes now from the getFoo() method. Here's a simple troublesome example, and the introduction to the opening sentiment of what to do with the default value.
int fooValue = new Foo().setFoo(null).getFoo();
OK, so that's a truncated overly simple example. What was really happening is that getFoo() was being used in some math, like fooValue = 5 + foo.getFoo();
, but these are really the same error with two faces.
So, perhaps then the right thing to do, and the workaround required to make the setFoo(null) and getFoo() work is to have an acceptable "undefined" value. This turns our simple Foo class into something like this:
public class Foo { private Integer foo = null; public void setFoo(Integer foo) { this.foo = foo; } public int getFoo() { return (foo == null) ? 0 : foo; } }
That getFoo() will return zero when foo is null or when foo is zero. There's not a way to tell the difference, but it allowed the code that now tried to use a null Integer to not break the math later.
To be fair, this is not the fault of Java or autoboxing. This is the fault of poor development in which a bean was constructed that could have a null value assigned to a primitive that was later always assumed to be valid. The more correct solutions are to not allow the bean to be created with the null value (protection from bad input) or to not use the bean later when there are missing values (protection from bad output). Back to the checkbook example, if the value for the balance is null, either don't make the Checkbook instance, or later, don't try to use the balance from the Checkbook.