Monday, August 31, 2009

Solutions to Two More Java Puzzlers

In my last post, Two More Java Puzzlers, I gave two puzzling snippets of Java code that don't act exactly as expected.

The first puzzle asked why the following code prints null instead of either "even" or "odd" as expected.

public static void main(String[] args)
{
String a = null;
int n = "number".hashCode();

switch( n % 2 ) {
case 0:
a = "even";
break;
case 1:
a = "odd";
break;
}

System.out.println( a );
}

This code uses n % 2 as the condition on a switch with cases for 0 and 1. Strangely, neither of the cases is executed. What other return values could the modulo operator produce when the divisor is 2? Surely 0 and 1 are the only two possible remainders when dividing by 2, aren't they?

Usually that's the case. "Usually" meaning, "for positive dividends." The Java % operator doesn't return the modulus in a strict mathematical sense. What it returns is the remainder.

What's the difference? In this particular case "number".hashCode() returns a negative value. When n is negative in the operation n % 2, the result will be negative too.

This isn't just a problem in Java. Some languages specify that the % operator is a strict modulus function, and some specify that it returns the remainder. You should know how the operator reacts to negative inputs in whatever language you're using.

To fix the code above, just use the absolute value of the result of the remainder operation.

The second puzzle asked why the following code crashes where it does, and why doesn't it crash sooner?

public static void main(String[] args)
{
Vector<Integer> numbers = new Vector<Integer>();

int min = 128;
int max = 255;

for( int i = min; i <= max; ++i )
{
numbers.add(i);
}

int element = 100;

numbers.remove(element);

element = 255;

if( numbers.contains(element) )
{
numbers.remove(element);
}
}

The code above creates a Vector of Integer objects, then adds the values from 128 to 255 to the Vector. It then removes element 100, checks to see if the Vector contains and element with the value 255, and attempts to remove it if it does. (At least this is what it appears to be doing.)

If you run the code above you will see it crash on line 21, when the second remove operation is attempted. Why does it crash there and not the first time it attempts to removes an element? The second element removed has a value of 255, which was added to the Vector. If that value wasn't found in the Vector, the remove operation wouldn't even execute. It should crash earlier, when the attempt to remove the element with the value of 100 is executed, if it's going to crash at all, shouldn't it? There is no element with that value to remove.

The problem with the above code is related to auto-boxing. More precisely, it's a lack of auto-boxing at the critical point that's causing the code to fail. Note that on line 10 when the int values are added to the Vector, they're automatically wrapped and added as Integer objects. This might lead you to believe that the same thing will happen when you try to remove elements from a Vector, as well. This isn't the case when the elements are Integers, though.

The problem is that Vector has a remove(int index) method that interferes with the auto-boxing that would normally occur when you try to remove an int. The statement on line 21 above is attempting to remove the element at index 255, which doesn't exist, instead of the element with the value 255, which does. The contains method isn't overloaded, so the condition on line 19 evaluates to true, allowing the fatal line of code to execute.

And what about the other remove operation? It has the same problem, only reversed. It appeared to be attempting to remove the element with the value of 100, which doesn't exist, but it was really removing the element at index 100, which does exist. This could be a particularly insidious bug if the values never go out of range. Instead of crashing quickly and alerting you to a problem, it could just quietly remove the wrong values.

To fix the bugs in this puzzler, just wrap the variable element in an Integer object before using it as an argument to remove, to make sure the expected method is called. And what lessons can we learn? Never assume. Always check to make sure you know exactly which operations will use auto-boxing, and which operations won't, with whatever libraries you use.

1 comment:

CurtainDog said...

Nice autoboxing fail. My preferred method of dealing with autoboxing is to make the use of it a compiler error and hit it with a big stick.