Tuesday, 18 September 2012

Puzzle 23: No Pain, No Gain


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!