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!