This program prints a word, using a random number generator
to select the first character. Describe the behavior of the program:
import java.util.Random; public class Rhymes { private static Random rnd = new Random(); public static void main(String[] args) { StringBuffer word = null; switch(rnd.nextInt(2)) { case 1: word = new StringBuffer('P'); case 2: word = new StringBuffer('G'); default: word = new StringBuffer('M'); } word.append('a'); word.append('i'); word.append('n'); System.out.println(word); } }
Solution 23: No Pain, No Gain
At first glance,
this program might appear to print out the words Pain, Gain,
and Main with equal likelihood, varying from run to run. It appears to
choose the first letter of the word, depending on the value chosen by the random
number generator: M for 0, P for 1, and G for 2. The
puzzle's title might have provided you with a clue that it doesn't actually
print Pain or Gain. Perhaps more surprisingly, it doesn't
print Main either, and its behavior doesn't vary from run to run. It
always prints ain.
Three bugs conspire to cause this behavior. Did you spot them
all? The first bug is that the random number is chosen so the switch
statement can reach only two of its three cases. The specification for
Random.nextInt(int) says: "Returns a pseudorandom, uniformly
distributed int value between 0 (inclusive) and the specified
value (exclusive)" [Java-API]. This means that the only possible
values of the expression rnd.nextInt(2) are 0 and 1.
The switch statement will never branch to case 2, which
suggests that the program will never print Gain. The parameter to
nextInt should have been 3 rather than 2.
This is a fairly common source of problems, known as a fencepost error. The name comes from the common but
incorrect answer of 10 to the question, If you build a fence 100 feet long with
posts 10 feet apart, how many posts do you need? Both 11 and 9 are correct
answers, depending on whether there are posts at the ends of the fence, but 10
is wrong. Watch out for fencepost errors.
Whenever you are working with lengths, ranges, or moduli, be careful to
determine which endpoints should be included, and make sure that your code
behaves accordingly.
The second bug is that there are no break statements
between the cases. Whatever the value of the switch expression, the
program will execute that case and all subsequent cases [JLS 14.11]. Each case
assigns a value to the variable word, and the last assignment wins. The
last assignment will always be the one in the final case (default),
which is new StringBuffer('M'). This suggests that the program will
never print Pain or Gain but always Main.
The absence of break statements in switch
cases is a common error that tools can help you catch. As of release 5.0,
javac provides the -Xlint:fallthrough flag to generate
warnings when you forget a break between one case and the next. Don't fall through from one nonempty case to another.
It's bad style because it's unusual and therefore confusing to the reader. Nine
times out of ten, it indicates an error. If Java had not been modeled after
C, its switch statement would probably
not require breaks. The lesson for language designers is to consider providing a
structured switch statement.
The last and most subtle bug is that the expression new
StringBuffer('M') probably does not do what you think it does. You may not
be familiar with the StringBuffer(char) constructor, and with good
reason: It does not exist. There is a parameterless constructor, one that takes
a String indicating the initial contents of the string buffer and one
that takes an int indicating its initial capacity. In this case, the
compiler selects the int constructor, applying a widening primitive conversion to convert the
char value 'M' into the int value 77 [JLS
5.1.2]. In other words, new StringBuffer('M') returns an empty string
buffer with an initial capacity of 77. The remainder of the program appends the
characters a, i, and n to the empty string buffer and
prints out its contents, which are always ain.
To avoid this kind of problem, use
familiar idioms and APIs whenever possible. If you must use unfamiliar APIs,
read the documentation carefully. In this case, the program should have
used the common StringBuffer constructor that takes a
String.
This corrected version of the program fixes all three bugs,
printing Pain, Gain, and Main with equal
likelihood:
import java.util.Random; public class Rhymes { private static Random rnd = new Random(); public static void main(String[] args) { StringBuffer word = null; switch(rnd.nextInt(3)) { case 1: word = new StringBuffer("P"); break; case 2: word = new StringBuffer("G"); break; default: word = new StringBuffer("M"); break; } word.append('a'); word.append('i'); word.append('n'); System.out.println(word); } }
Although this program fixes the bugs, it is overly verbose.
Here is a more elegant version:
import java.util.Random; public class Rhymes { private static Random rnd = new Random(); public static void main(String args[]) { System.out.println("PGM".charAt(rnd.nextInt(3)) + "ain"); } }
Better still is the following version. Although slightly
longer, it is more general. It does not depend on the fact that the possible
outputs differ only in their first characters:
import java.util.Random; public class Rhymes { public static void main(String args[]) { String a[] = {"Main", "Pain", "Gain"}; System.out.println(randomElement(a)); } private static Random rnd = new Random(); private static String randomElement(String[] a) { return a[rnd.nextInt(a.length)]; } }
To summarize: First, be careful of fencepost errors. Second,
remember to put a break after each case in switch statements.
Third, use common idioms and APIs, and consult the documentation when you stray
from the well-worn path. Fourth, a char is not a String but is
more like an int. Finally, watch out for sneaky puzzlers.
No comments:
Post a Comment
Your comments are welcome!