Section 6.5

Multiple Choice


It would be wonderful if every decision could be reduced to a single "either-or" choice that could be solved with a simple if-else statement; it would sure make programming easier!

A decision from an IRS pamphlet. Most of the time, though, things are more complex, and you'll need to write selection statements involving three, four, five or more alternatives. Just writing a program to calculate your income tax, for instance, using the tax tables, involves dealing with literally hundreds of different alternatives.

To solve these kinds of programs you'll need to learn how to use one of Java's multi-way branching statements:

Let's start by looking at sequential if statements, which are used when you have a sequence of related comparisons.

Sequential If Statements

CS 170 grading scale. One sequential comparison that you're all familiar with is the "letter grading scale" used to assign marks in school, (including in this course), similar to that shown in the table to the right.

Typically, your letter grade is based on a percentage representing the weighted average for all of the work you've done during the term. In the example shown here, for instance, those who earn 90% or greater receive 'A's, those with percentages less than 90%, but at least 80% earn 'B's, and so on.

Running GradeCalculator. To explore sequential comparisons I'll use a simple example program named GradeCalculator.java which you'll find in this chapter's code folder. Open it up and follow along in DrJava if you want to experiment as we go.

If you run the program, you'll see that it:

As you can see here, I've run the program for a student named Fred. I entered two 100-point test scores and a 15-point quiz score and the program correctly calculates Fred's percentage score (79.1%). However, it doesn't yet handle the letter grade. That's our next stop.

Before we get there, though, let's take a look at what we've got; we'll read through the program together, and then we'll look at several different ways we can implement the code for calculating the letter grade.

The GradeCalculator Program

Here's how the program works:

  1. Lines 19 and 20 print a heading for the program, and then lines 23 and 24 create a String variable named student and has the user initialize it by reading the student's name. Lines 25 and 26 create an int variable named numScores and has the user initialize it by reading the number of scores to record.
    The GradeCalculator lines 17-22.
  2. Lines 29 and 30 define the accumulators for the program. As each score is entered, that score will be added to the pointsPossible and pointsEarned variables. I made these variables double instead of int so the teacher could assign half points.
    The GradeCalculator lines 25-26.
  3. Next, we need to get the input for each exam, quiz or lab, which we'll do by using the for loop. Notice that we repeat the input actions a fixed number of times, based on the value of the variable numScores:
    The GradeCalculator lines 29-38. After all of the input is done, the points earned and the possible points are added up and the result printed.
  4. After the loop, line 48 uses the possible points and the points earned to calculate the student's percentage grade. This is done by calling the getPercentage() function, which we'll look at shortly.
    The GradeCalculator lines 42-43. Then, line 49 defines the String variable letterGrade and initializes it by calling the function getLetterGrade() passing the percent variable initialized in the previous line.
  5. Finally, the main() method ends by printing the formatted output in the statement that spans lines 52-53. The output consists of the student's name, left-aligned in a 40-character-wide field, the student's percentage grade (formatted to 1 significant decimal), and the letter grade.
    The GradeCalculator line 46.

Now, let's look at the two functions.

The getPercentage() Function

The getPercentage() method returns the student's current numeric grade, as a percentage as the name implies. There function takes two double parameters representing the possible points and the points that the student has earned. The getPercentageFunction.

The method uses a simple if-else statement to avoid the problem of returning the NaN value when dividing by floating-point zero if a student hasn't yet taken any exams and 0.0 is passed as the possible parameter. (If the parameter were an int instead of a double, you'd still need the if-else statement do avoid the runtime error that occurs with an integer divide-by-zero.)

In the else clause, the percentage returned is multiplied by 100.0 so that we'll deal with numbers like 79.0, not .79.

The getLetterGrade() Function

The final portion of code we want to look at is getLetterGrade() which returns the letter grade based on the student's current percentage score. As you can see here, the function currently returns the phrase "NOT DEFINED":
The getLetterGrade() function.

Multiway Selection Techniques

Now let's look at several techniques for implementing the getLetterGrade() function.

Method 1: Independent if Statements

The first technique is to use the logical operators to combine several conditions using independent if statements. Here's what the code would look like in this version of the function:
Independent if statements.

Analysis

Here's how the method works:

  1. Since this is a String function, a String output variable named result is created and initialized to the empty String. Then, the literal "NOT DEFINED" in the return statement is replaced with the variable result.
  2. Next, the current numeric percentage is compared against each range, using the && operator to check the lower and upper bound for the range. I've used the percentage cutoffs for a common course. (Since each if body contains only a single statement, I've also used the single-line formatting technique that I mentioned in the last unit.)
  3. Finally, before returning the result, the function makes sure that any invalid input is reported as well.

While this solution is correct, it's also longer than it needs to be and it's quite inefficient. Each time the function is called, all six Boolean selection conditions are tested, even after the correct one has been found.

Independent if statements. The problem is, the solution uses independent if statements, while the conditions themselves are actually interdependent. If someone gets an "A", then it is not possible for them to also, and at the same time, get an "F".

Independent if statements are designed for processing conditions where each condition could be independently true, and not really affect any other condition, like the code you'd use to process the checkboxes on a form like that shown here.

With a form like this, the user may select any number of options, (or none at all), but each option doesn't affect the others. That means you must check each one, and the only way to do that is by using independent if statements.

Method 2: Using Multiple Selection

The Multiple Selection algorithmic pattern. A better solution, when you need to select one course of action from many possible alternatives (which is the case here), is to employ what Rick Mercer at Arizona State calls the "Multiple Selection Algorithmic Pattern", shown here. An "algorithmic pattern" is simply a recipe for solving a particular category of problem.

This particular pattern is also often called a ladder style if statement, because each of the conditions are formatted one under the other, like the rungs on a ladder. It is also sometimes called an if-else-if statement. (Note though that the else if keywords are not combined into a single elseif, as in some other languages, such as Visual Basic, or the elif keyword in Python.)

Here's the code for this second version of getLetterGrade(), using the multiple-selection (or sequential if) pattern :
Multiple-selection (or sequential) if statements.

Analysis

  1. With the multiple-selection pattern, we need to move our error checking to the beginning of the routine from the end, since each test depends on the one that precedes it. If we left the error checking at the end of the method, it would never be executed, and the method would return an incorrect value. In the example shown here, I've used concatenation to display the percent value that causes the problem, along with the error message.
  2. On the following lines, only the lower bounds of each category is tested. By the time we reach line 86, we already know that the percentage falls between 0% and 100%, so we only have to check if it is at least 90% to assign the value for an "A".

    The same pattern continues on lines 87 through 89. On line 87, we know that the percentage is less than 90, since we just checked it on the previous lines. So, we don't need to check it again, like we did with the independent if example.

  3. Finally, the last "rung" in the ladder doesn't need an if portion. The only thing left is the range from 0 to 49, so you can use a plain else.

A Pitfall to Watch For

While the multiple-selection pattern is much more efficient than the independent if statements, each of the statements is dependent on the one that comes before it. That means that the order of the statements is significant.

If you switch the order of the "A" and "B" tests, for instance, like this:

else if (percent >= 80) result = "B";
else if (percent >= 90) result = "A";

all of those students who should have gotten "A"s, will get "B"s instead, since a score of 95 matches the condition (percent >= 80) and that condition is now encountered before the condition that tests for an "A". The test for an "A" will never be executed.

Try It Yourself

Go ahead and try it out. Complete the letterGrade() function and then run it with inputs for each possibility (A-F along with out of range).

Nested If Statements

Russian nesting dolls. As you just saw, you can code decisions involving multiple alternatives in two different ways:

A third way to do the same thing is through nesting.

Nesting means that one if statement is embedded or nested inside the body of another if statement, much like the traditional Russian nesting dolls, shown here. The nested if statement can appear in the body of the if portion of an if-else statement, in the body of the else portion, or inside both.

Calculating Taxes

Nested if statements are used whenever you need to test for different levels of decision making, rather than a set of purely sequential tests. If you're one of those fortunate folks making more than a hundred thousand dollars a year, for instance, you'll need to calculate your taxes using the following formula, instead of using the tax tables:

Filing Status : Single Filing Status: Married Filing Jointly
Taxable IncomeTax Taxable IncomeTax
$100,000 ... $146,750 28% less $5,373 $100,000 ... $117,250 25% less $6,525
$146,751 ... $319,100 33% less $12,710.50 $117,251 ... $178,650 28% less $10,042.50
Over $319,100 35% less $19,092.50 $178,651 ... $319,100 33% less $18,975
    Over $319,100 35% less $25,357
Filing Status : Married Filing Separately Filing Status: Head of Household
Taxable IncomeTax Taxable IncomeTax
$100,000 ... $159,550 33% less $9,487.50 $100,000 ... $100,500 25% less $4,400
Over $159,550 35% less $12,678.50 $100,501 ... $162,700 28% less $7,415
    $162,701 ... $319,100 33% less $15,550
    Over $319,100 35% less $21,932

Suppose that you were single, with a taxable income of $159,750. To calculate your tax, you'd first locate the schedule for your filing status (Single), and then locate the bracket you fall into (the second). Your tax would be $159,750 x 33% less $12,710.50.

If you wanted to write a program to perform this calculation, you'd need to follow a similar set of steps. You'd first need to use a set of sequential if statements to determine which set of calculations to use, like this:

if (status == SINGLE)
{
    // calculate the tax for a single taxpayer
}
else if (status == MARRIED_JOINT_FILE)
{
    // calculate for a joint-filing married taxpayer
}
else if (status == MARRIED_SINGLE_FILE)
{
    // calculate for a single-filing married taxpayer
}
else if (status == HEAD_OF_HOUSEHOLD)
{
    // calculate for a head-of-household taxpayer
}

Then, nested inside the body of each portion of each if statement, you'd test the various income levels, like this:

if (status == SINGLE)
{
    // calculate the tax for a single taxpayer
    if (taxableIncome <= SINGLE_BRACKET1)
        tax = taxableIncome * SINGLE_RATE1 - SINGLE_EX1;
    else if (taxableIncome <= SINGLE_BRACKET2)
        tax = taxableIncome * SINGLE_RATE2 - SINGLE_EX2;
    else
        tax = taxableIncome * SINGLE_RATE3 - SINGLE_EX3;
}
else if (status == MARRIED_JOINT_FILE)
{
    // calculate the tax for a married taxpayer
}

Nesting Pitfalls: The Dangling else

When you nest one if statement inside of another, the nested if statement won't always have an else portion. When this occurs, it's possible for the else clause of the enclosing block to inadvertently "attach itself" to the inner if statement. This is known as the dangling else problem.

Here's an example. In the code shown in the screenshot, the programmer intended to allow children 12 and under to "get in free" (the else branch of the first if statement. A nested if statement was intended to provide a "senior citizens" discount as well. Everyone else was supposed to pay full price.
The dangling else problme. When the code is run, though, the result is exactly the opposite. Because the else clause automatically attaches itself to the closest unmatched if statement, the young kids all pay full price, while all of the pre-senior adults get in for free.

Avoiding this problem is really pretty easy: whenever you use nested if statements, always enclose the body of both the if and else portions in braces. The braces act like a "fence" that keeps the else portions where they belong.

The switch Statement

Both the nested if statements and the ladder-style if-else-if statements, provide you with the tools you need to do multi-way branches. But there's still one more multi-way selection tool that you should have in your programming toolbox.

A telephone switch. Another Java control structure, the switch statement, provides an even more efficient way to select between several different alternatives, provided you can live with its limitations and somewhat idiosyncratic behavior.

Many other languages have a multi-way branch statement, usually called a case statement. If you've programmed in one of these different languages, you might find the term switch a little unusual. The name seems much more logical, however, if you think of a railway switching yard or a telephone switchboard, rather than a light-switch.

The switch statement is different than the if-else statement because the switch is based upon an integer evaluation, rather than upon a boolean test.

Let's briefly look at the syntax of the switch statement and then we'll take a look at how it works.

Switch Syntax

Here's an illustration that shows the syntax of the switch statement:
The syntax of the switch statement As you can see, the switch statement consists of five different parts.

  1. The switch keyword, followed by an integral expression enclosed in parentheses. This expression is called the switch selector. The selector can be a variable or any expression, as long as the type of the expression is byte, short, int, or char. You cannot use floating-point, long or String selectors. (The upcoming Java 7, will allow String selectors, however.) The body of the switch statement, which follows, is enclosed in braces.
  2. Any number of case labels. Each case label consists of the keyword case, followed by a constant integral expression. The case label ends with a colon, not a semicolon.
  3. Any number of statements following each case label. These do not have to be enclosed in braces as with a loop or if body.
  4. A break statement to end each case.
  5. A default: label to handle the unmatched cases.

Let's look at each of these pieces to see how they work together.

How it works

When a switch statement is encountered, the first thing that happens is that its switch selector, or integer "test" expression is evaluated. After the test expression is evaluated the following steps are performed:

  1. The list of labeled constants is scanned, looking for a match. If an exact match is found:
    • Java begins executing the first line of code immediately following the matching case label.
    • Each line of code following the case label is executed sequentially until either a break statement is encountered or all of the statements in the switch body have been executed.
  2. If no match is found, then execution jumps to the statement following the default: label, if one was supplied.
  3. If no match is found and there is no default: label, then execution jumps to the first statement following the switch body.

A Simple Example

A very common use for a switch statement is to implement a menu. Let's create a simple menu that allows the user to select 1) for Twinkies, and 2) for Tofu; something for everyone. Here's the code:
The SimpleMenuSwitch program. You can find this in the program SimpleMenuSwitch.java in this chapter's code folder. Let's look at the code:

  1. The first section of the program creates a Scanner object for input and then prints a menu.
  2. After printing the menu, the program creates an int variable named choice and allows the user to initialize it.
  3. The choice variable is used as the switch selector.

    The switch statement can evaluate int, byte, short, or char selection values. It will not work with double, long, String, or any object types.

  4. Each case label contains a constant value to be matched against the switch selector. The value stored in choice will be compared to each of the case label values and if an exact match is found, the program will jump to the first line of code in that block.
  5. Each case block should (almost always) end with a break statement. If you don't have a break then control will continue to the next case block, even if the selector does not match that block's case label.
  6. Finally, if you want to do something if the switch selector fails to match any case, then you can add an optional default block. This block is traditionally placed last (which is why I haven't included a break statement). If you don't have a default block then nothing would happen at all when the user entered an invalid selection.

Here's what the program looks like as it runs:
Running the SimpleMenuSwitch app.

Switch Pitfalls

There are two common problems students encounter when learning to use the switch statement.

The first problem has to do with the case labels. Each case label must include:

The second problem has to do with forgetting to use a break statement. If you don't include a break statement, then all of the statements in the switch body, following your case are executed as well. This is called fall through and is a very common programming error when using a switch statement.

Dragnet.

Multi-way Branches Summary

  • Independent if statements should only be used when each condition is truly independent.
  • Sequential or ladder-style if-else-if statements allow you to efficiently choose one from several different interdependent conditions. This structure is also known as the Multiple-Selection Algorithmic Pattern.
  • One pitfall of sequential if statements, is that the order of the conditions is critical. Placing an item out of order can result in an incorrect test.
  • When you have different levels of testing, then the best kind of structure is a set of nested if statements.
  • A common pitfall with nested if statements is having a nested if without an else, where an "outer" else clause attaches itself to the "inner" if. Using braces on all nested if statements will eliminate this problem.
  • The switch statement is a multiway branch that relies on matching labeled blocks of code with an integral expression. Instead of a Boolean condition, an implicit equality check is made to each label, and an unconditional jump is performed if a match is found.
  • The switch selector, or variable that is evaluated, must be one of the integer types (not including long).
  • The case labels used to identify each code block must be literals or constants, followed by a colon (not a semicolon).
  • The default block, if supplied, is executed when no case matches the selector.
  • You must supply a break statement to terminate each case block. If you don't, then execution falls through to the following case blocks.