Tuesday, 18 September 2012

Puzzle 42: Thrown for a Loop


The following program loops through a sequence of int arrays and keeps track of how many of the arrays satisfy a certain property. What does the program print?
public class Loop {

    public static void main(String[] args) {

        int[][] tests = { { 6, 5, 4, 3, 2, 1 }, { 1, 2 },

                          { 1, 2, 3 }, { 1, 2, 3, 4 }, { 1 } };

        int successCount = 0;

        

        try {

            int i = 0;

            while (true) {

                if (thirdElementIsThree(tests[i++]))

                    successCount++;

            }

        } catch (ArrayIndexOutOfBoundsException e) {

            // No more tests to process

        }

        System.out.println(successCount);

    }



    private static boolean thirdElementIsThree(int[] a) {

        return a.length >= 3 & a[2] == 3;

    }

}


Solution 42: Thrown for a Loop

The program tests each element of the array tests with the thirdElementIsThree method. The loop through this array is certainly not traditional: Rather than terminating when the loop index is equal to the array length, the loop terminates when it attempts to access an array element that isn't there. Although nontraditional, this loop ought to work. The thirdElementIsThree method returns true if its argument has three or more elements and the third element is equal to 3. This is true for two of the five int arrays in tests, so it looks as though the program should print 2. If you ran it, you found that it prints 0. Surely there must be some mistake?
In fact, there are two mistakes. The first mistake is that the program uses the hideous loop idiom that depends on an array access throwing an exception. This idiom is not only unreadable but also extremely slow. Do not use exceptions for loop control; use exceptions only for exceptional conditions [EJ Item 39]. To correct this mistake, replace the entire try-finally block with the standard idiom for looping over an array:
for (int i = 0; i < tests.length; i++)

    if (thirdElementIsThree(tests[i]))

        successCount++;


If you are using release 5.0 or a later release, you can use the for-each construct instead:
for (int[] test : tests)

    if (thirdElementIsThree(test))

        successCount++;


As bad as the first mistake is, it alone is not sufficient to account for the observed behavior. Fixing this mistake will, however, help us to find the real bug, which is more subtle. If we fix the first mistake and run the program again, it fails with this stack trace:
Exception in thread "main"

java.lang.ArrayIndexOutOfBoundsException: 2

    at Loop.thirdElementIsThree(Loop.java:19)

    at Loop.main(Loop.java:13)


Clearly, there is a bug in the thirdElementIsThree method: It is throwing an ArrayIndexOutOfBoundsException. This exception was previously masquerading as the end of the hideous exception-based loop.
The thirdElementIsThree method does return TRue if its argument has three or more elements and the third element is equal to 3. The problem is what it does when these conditions do not hold. If you look closely at the boolean expression whose value it returns, you'll see that it is a bit different from most boolean AND operations. The expression is a.length >= 3 & a[2] == 3. Usually, you see the && operator used under these circumstances. This expression uses the & operator. Isn't that the bitwise AND operator?
It turns out that the & operator has another meaning. In addition to its common use as the bitwise AND operator for integral operands, it is overloaded to function as the logical AND operator when applied to boolean operands [JLS 15.22.2]. This operator differs from the more commonly used conditional AND operator (&&) in that the & operator always evaluates both of its operands, whereas the && operator does not evaluate its right operand if its left operand evaluates to false [JLS 15.23]. Therefore, the thirdElementIsThree method attempts to access the third element of its array argument even if it has fewer than three elements. Fixing this method is as simple as replacing the & operator with the && operator. With this change, the program prints 2 as expected:
private static boolean thirdElementIsThree(int[] a) {

    return a.length >= 3 && a[2] == 3;

}


Just as there is a logical AND operator to go with the more commonly used conditional AND operator, there is a logical OR operator (|) to go with the conditional OR operator (||) [JLS 15.22.2, 15.24]. The | operator always evaluates both of its operands, whereas the || operator does not evaluate its right operand if its left operand evaluates to TRue. It is easy to use the logical operator rather than conditional operator by accident. Unfortunately, the compiler won't help you find this error. Intentional uses of the logical operators are so rare that all uses are suspect; if you really want to use one of these operators, make your intentions clear with a comment.
In summary, do not use the hideous loop idiom where an exception is used in preference to an explicit termination test; this idiom is unclear, slow, and masks other bugs. Be aware of the existence of the logical AND and OR operators, and do not fall prey to unintentional use. For language designers, this is another example where operator overloading is confusing. It is not clear that there is a case for providing the logical AND and OR operators in addition to their conditional counterparts. If these operators are to be supported, they should be visually distinct from their conditional counterparts.

No comments:

Post a Comment

Your comments are welcome!