Hands on Testing Java: Lab #7

Selection Statements

Introduction

One of the easiest ways to make a program user-friendly is to make it menu driven. That is, rather than prompting the user in some vague sort of way, we present them with a menu of the choices available to them. Then all the user has to do is look at the menu and choose one of the choices. Since the menu always tells the user what their options are, the user needs no special knowledge, making such a program easy to use.

For example, a simple 4-function calculator program might prompt the user by providing the following menu:

Please enter:
      + to add two numbers;
      - to subtract two numbers;
      * to multiple two numbers; or
      / to divide two numbers.
   -->

Thanks to the menu, a user knows exactly what to enter.

Today's exercise is to complete such a program, and at the same time learn about some of the Java control structures.

Getting Started

Do this...

Add the appropriate Javadoc comments to these files.

Calculator Objects

The Calculator class will implement an object-generating class for calculator objects. The strange thing about the class, though, is that it won't have any instance variables.

Think about it: what are the attributes of a calculator? Sure, you see buttons, but that's an input consideration. We're modeling the internal workings of a calculator. You'll use methods and parameters to send data to our calculator objects. An attribute is something that the object has to remember.

Many calculators do have a memory of some sort that will save away one value (or more?) for you, but we're not interested in those kinds of calculators in this lab. At the end of this lab, you could go back and implement a memory for the calculator as well as a dozen other features. This maintenance is really easy to do, though, as long as you start out simple and with a true object design, not with static methods and no instances.

So while the object-oriented design might seem like overkill for now, it is good in general, not just for practice.

Calculator Constructor

It is a good idea to provide at least one constructor for any class, and since a Calculator object has no instance variables, a default constructor (that does nothing!) is sufficient. The compiler needs to be reassured that it should do nothing when it constructs a new Calculator object.

Do this...
So define a default constructor that does nothing (i.e., an empty body) in the Calculator class.

The one thing this class will provide is an instance method (or two or three) named apply(); this will apply an arithmetic operator (e.g., '+' or '/') to two real-number values. But first, we turn to the driver and test-case class.

Calculator Driver

The CalculatorDriver class implements a driver with this behavior:

Behavior of CalculuatorDriver#main(String[])

Our program should display on the screen a greeting, followed by a menu of permissible operations. It should then read a user-specified operation from the keyboard. It should then prompt the user for the two operands for that operation, and then read those operands from the keyboard. It should then compute the result of applying the user-specified operation to the two operands with a calculator. It should conclude by displaying that result on the screen, with appropriate labeling.

Do this...
Look over the main driver to make sure it's implementing this behavior.

Some of the code in this driver is commented out to avoid compilation errors. You'll uncomment that code eventually.

Calculator Test Case

Since you're writing a computation method, you need test methods to test this computation. The CalculatorTest class will implement the tests. First, you need to set up the class for testing an instance.

Do this...
Create an instance variable in the CalculatorTest class named myCalculator.

Since all calculators do the same things and have no distinguishing attributes, there's no reason to test more than one instance.

Do this...
Write a setUp() method for CalculatorTest that initializes myCalculator using the default Calculator constructor.

Here's your last reminder of what the setUp() method should look like:

setup-method pattern
@Override
public void setUp() {
  initializations
}
  • initializations are assignment statements initializing the test-case class's instance variables.

You will end up writing three methods for the Calculator class which will all compute the same value. So that you're sure to test all three methods all of the time, you need a special "assert" method.

Do this...
Add this method to CalculatorTest:

public void assertApplies(String message, double expected, char operation, double operand1, double operand2) {
    assertEquals(message, expected, myCalculator.apply(operation, operand1, operand2), 1e-8);
    assertEquals(message, expected, myCalculator.applyIf(operation, operand1, operand2), 1e-8);
    assertEquals(message, expected, myCalculator.applySwitch(operation, operand1, operand2), 1e-8);
}

The method calls will cause compiler errors because you haven't declared them yet. From the tests above, this minimum specification should be relatively clear:

Specification of Calculator#apply*(char,double,double) methods:

receive: operation, a char; operand1 and operand2, two double values.
return: a double.

Do this...
Write the three method stubs to satisfy the compiler. Return Double.NaN.

Method Design

The responsibility of these three methods of the Calculator class is to apply an arithmetic operation to two values (known as "operands"). We'll design this responsiblity in terms of just the apply() method for now:

Behavior of Calculator#apply(char,double,double)

I am a calculator. When I am told to apply a computation, I receive an arithmetic operation and two operands. I compute a result based on the operation:

  • If the operation is '+', the result is the sum of the two operands.
  • If the operation is '-', the result is the difference of the two operands.
  • If the operation is '*', the result is the product of the two operands.
  • If the operation is '/', the result is the quotient of the two operands.
  • If the operation is none of these, I complain.

I finish by returning the result.

From this behavioral description, we can identify the following objects:

Objects of Calculator#apply(char,double,double)
Description Type Kind Movement Name
The arithmetic operation char varying received operation
One of the operands double varying received operand1
The other operand double varying received operand2
The result double varying returned result
A complaint IllegalArgumentException literal thrown ---

Now we can be more specific about the specification:

Specification of Calculator#apply(char,double,double):

receive: operation, a char; operand1 and operand2, two double values.
return: the real-number result of applying operation to operand1 and operand2.

From our behavioral description, we have these operations:

Operations of Calculator#apply(char,double,double)
Description Predefined? Name Library
Receive operation yes parameter built-in
Receive operand1 yes parameter built-in
Receive operand2 yes parameter built-in
Compute the sum of operand1 and operand2 yes + built-in
Compute the difference of operand1 and operand2 yes - built-in
Compute the product of operand1 and operand2 yes * built-in
Compute the quotient of operand1 and operand2 yes / built-in
"the result is..." yes = built-in
Do exactly one of the computations ??? ??? ???
Return the result yes return built-in
Complain yes throw built-in

Design Task #7.01 Fill in the ???s from this operations lists based on what you learned from a previous lab.
To be answered in Design/lab07.txt.

As indicated, Java provides facilities for performing each of these operations, and you've seen most of them before. Doing just one of the computations though involves selective execution---you want your code to select what code is executed; this is also known as selective behavior. There are actually two ways to solve this particular problem in Java, and you will implement both solutions in the other apply*() methods. But first, we finish the OCD:

Algorithm of apply() in Calculator
  1. Receive operation, operand1 and operand2.
  2. If operation is '+':
    1. Set result to be operand1 + operand2.
    Otherwise, if operation is '-':
    1. Set result to be operand1 - operand2.
    Otherwise, if operation is '*':
    1. Set result to be operand1 * operand2.
    Otherwise, if operation is '/':
    1. Set result to be operand1 / operand2.
    Otherwise,
    1. Complain that operation is invalid.
    End if.
  3. Return result.

This algorithm uses a pseudo-code form of the if statement that has multiple branches. But, as mentioned earlier, Java provides two different statements for coding up this particular algorithm. applyIf(char,double,double) will implement one solution, and applySwitch(char,double,double) will implement the other. apply(char,double,double) itself will call one or the other.

This situation actually is not uncommon; it's actually a perfect example of encapsulation. Anyone who uses our Calculator class should use the apply() method, and they don't have to worry about how the selection is implemented---those details are hidden, the very definition of encapsulation.

This will allow you to experiment between two different Java statements while allowing anyone to still use your Calculator class. Professional developers do this all of the time. You've set up your test-case class so that you can test all three methods to make sure that they're all computing the same values.

Testing

Do this...
Write four test methods in CalculatorTest: testApplyAdd(), testApplySubtract(), testApplyMultiply(), and testApplyDivide(). Use the assertApplies(double,char,double,double) in these test methods so that all three computation methods are tested. Write at least three assertions for each test method; be sure to include some negative numbers.

For example, here's an assertion I might write:

assertApplies("2.0 + 3.0 is 5.0", 5.0, '+', 2.0, 3.0);

Do this...
Run your unit tests, and watch them fail miserably.

Common Coding

The apply() method should return the result of calling applyIf(), passing its parameters as arguments to applyIf(). This is one line of code. The other two methods should both return Double.NaN.

Do this...
Change apply(char,double,double) to invoke applyIf(char,double,double) and return that value. Rerun your tests to make sure they're still failing because of bad return values.

The driver can be compiled, too:

Do this...
Uncomment the lines in the driver in CalculatorCLIDriver. You should not get any errors when you compile. When you run the driver, anything you enter should result in Double.NaN.

Now to write good code for Calculator#applyIf(char,double,double) and Calculator#applySwitch(char,double,double).

The first step of the algorithm (receiving values) is taken care of by the method declaration itself.

The last step (to return result) is easy.

Do this...
Add the last step of the algorithm to both methods.

You'll get an error that result isn't declared yet. As always, before you can use a variable, you must declare it.

Do this...
As the first statement in both methods, declare result and initialize result to Double.NaN. Now you should be able to compile the code without any errors and run test-case class (still for a red bar).

The key is the second step of the algorithm, of course.

Using the if Statement

Selective behavior can be acheived by using the if statement, and we've already looked at two different forms of this statement:

single-branch-if-statement pattern
if (condition) {
  thenStatements
}
  • condition is a Boolean expression that determines whether or not thenStatements are executed.
  • thenStatements is one or more Java statements.
two-branch-if-statement pattern
if (condition) {
  thenStatements
} else {
  elseStatements
}
  • condition is a Boolean expression that determines whether thenStatements or elseStatements are executed.
  • thenStatements and elseStatements are one or more Java statements.

An if statement selectively executes the then clause or the else clause. These terms arise due to the meanings of the statements: if the condition is true, then the then-clause is executed, or else the else-clause is executed. When the else-clause does not exist, the decision is only whether or not to execute the then-clause.

Strictly speaking, the curly braces in these patterns are not necessary as long as there is only one statement in the corresponding thenStatements or elseStatements. These curly braces constitute a compound statement (a.k.a. a statement block)---a list of statements that the compiler treats as one statement. Most Java conventions, though, insist on the curly braces even when they are not strictly necessary. It's a worthwhile habit to pick up now.

If we chain two-branch ifs, we end up with a multi-branch if:

multi-branch-if-statement pattern
if (condition1) {
  statementList1
} else if  (condition2) {
  statementList2
...
} else if (conditionn) {
  statementListn
} else {
  statementListn+1
}
  • Each conditioni is a Boolean expression.
  • Each statementListi is a list of Java statements.
  • When conditions condition1 through conditionk are false and conditionk+1 is true, only statementListk+1 is executed.
  • If all conditions are false, only statementListn+1 is executed.

This form is called a multi-branch if since it allows the selection of one of multiple sections of code.

The key to interpretting a multi-branch if is that each if statement (except for the first) is the elseStatement of the previous if. This is one case where you definitely leave the curly braces off the elseStatements; otherwise, the Java code is indented unnecessarily to the right.

Each condition is evaluated in the order listed, until the first condition that evalutes to true; the corresponding statement list is executed, and then multi-branch if is done.

This multi-branch if provides exactly the behavior we need to perform step 1 of our algorithm---it's the pseudo-code we used to write that step!

Do this...
Add a multi-branch if to your applyIf() just before the return statement to encode the second step of the algorithm.

All of the "set" steps should be simple assignments to result.

Question #7.01 Does the compiler require you to use curly braces in this multi-branch if? Why or why not?
To be answered in Exercise Questions/lab07.txt.

Indicating that there's an error (the last statement of the multi-branch if) should be done by throwing an exception:

throw new IllegalArgumentException("Invalid operation: " + operation);

You concatenate the value of operation to the message so that the user has an idea what operation the method got; this is certainly helpful in debugging. Incidentally, we do not need to indicate in these exception messages where in the code the error was discovered since Java automatically adds this information to the exception object. You may have noticed this feedback whenever an exception is printed out to the screen.

Do this...
Make sure the exception is thrown as the default else in the multi-branch if.

Be sure that apply(char,double,double) calls applyIf(char,double,double). At this point, now that you have some good computation going, it'd be helpful to see a green bar from your tests.

Do this...
Comment out the call to applySwitch(char,double,double) in CalculatorTest#assertApplies(). Then compile your code, and run your test-case class for a green bar.

Do this...
Now uncomment the call to applySwitch(char,double,double), and run the tests for a red bar, again.

Using the switch Statement

From your testing you now know that the multi-branch if statement is functionally correct; that is, it solves the problem. However, it suffers from a slight performance drawback:

In general, selecting statementListi using a multi-branch if statement requires the evaluation of i conditions. Since evaluation of each condition consumes time, statements that occur later in the multi-branch if statement take longer to execute than do statements that occur earlier.

Now for your code, it's hardly worth getting upset by the "bad" performance of this multi-branch if. But in a program where the multi-branch if has hundreds or even thousands of conditions, this performance drawback can be quite severe!

A more important problem (which does apply to even your code) is that a multi-branch is a bit awkward to write and read.

In certain situations, you can use the more-efficient switch statement. Its general pattern looks like this:

switch-statement pattern
switch (expression) {
case CONSTANT1:
  statementList1
case CONSTANT2:
  statementList2
...
case CONSTANTn:
  statementListn
default:
  statementListn+1
}
  • expression is an expression that evaluates to an integer-compatible value.
  • Each statementListi is a sequence of valid Java statements. It can be empty.
  • Each CONSTANTi is an integer-compatible constant (actually a constant or literal).

When execution reaches a switch statement, the following actions occur:

  1. The expression is evaluated.
  2. If the resulting value is one of CONSTANTis, then execution begins in statementListi and proceeds through every statementListj (for j > i) until the very end of the switch statement.
  3. If the value of expression is not mentioned in any case, then the default statementListn+1 is executed.

The default clause is useful for handling general cases and (when there is no general case) for debugging. Even if you're certain that you have all cases covered, write a default clause that at least prints an error message (or, better yet, throws an exception).

Do this...
Re-read that second step.

Picky, Picky Switch Statements: Fall-Through Behavior

One of the most important things to note about a switch statement is that a case statement is only a starting point. A case label tells the program where to start executing the code. Once it starts, it goes through to the end of the switch statement unless we tell it otherwise.

And that's the first key to using a switch statement. Generally, this fall-through behavior (also known as drop-through behavior) isn't particular useful. Nearly always every statementListi will end with one of the following statements:

The only time you will want to leave out one of these statements to avoid fall-through behavior is when you really want the fall-through behavior. This is pretty rare (at least for beginning programers) unless it makes sense to leave the whole statement-list empty. This is useful when multiple values should result in the same behavior. Take this example:

switch (input) {
case 'a':
case 'A':
  theScreen.println("You typed in an 'a'.");
  break;
case 'b':
case 'B':
  theScreen.println("Don't type in an 'b'!");
  break;
}

It's a silly example, but it demonstrates how the fall-through behavior can be useful.

Picky, Picky Switch Statements: Integer-Compatible Values

Helpful hint: Do not use a switch statement for real-number or reference datatypes. This is a bit tricky because Java will compile a switch statement using reference types, but it will not work the way you want it to.

Another most important thing to note about a switch is the integer-compatible issue: both the expression of the switch and the CONSTANTSs in the case labels must be integer compatible. The integer-compatible types include the integer data types (e.g., int), the char data type, and even the boolean data type. Practically speaking, you use only ints and chars in the conditions of a switch.

Keep in mind that this restriction applies only to the expression and cases of a switch. You're free to compute with whatever values you want in the statement lists.

Picky, Picky Switch Statements: Equality Tests

A switch statement tests only equality. These two statements are equivalent to each other:

Multi-branch if

switch

type variable = expression;
if (variable == constant1) {
  statementList1
} else if (variable == constant2) {
  statementList2
} else if ...
...
else if (variable == constantn) {
  statementListn
} else {
  statementListn+1
}
switch (expression) {
case constant1:
  statementList1
  break;
case constant2:
  statementList2
  break;
...
case constantn:
  statementListn
  break;
default:
  statementListn+1
}

So if you have equality tests comparing the same expression with constants, you can use a switch. If you want to use a variable instead of a constant, you cannot use a switch; if you want a non-equality test (e.g., "less than", "greater than or equal to"), you cannot use a switch; if you want to test multiple expressions, you cannot use a switch.

Picky, Picky Switch Statements: All Together Now!

So, abusing the superlative, that's three most-important things you have to remember about the switch statement:

  1. Each statementListi should end with break, return, or throw.
  2. The data type being tested must be integer compatible.
  3. The comparison must be an equality test of the value of one expression with different constants.

Let's bring this all back to your code for applySwitch().

  1. You should make sure there's a break in each statement list of the switch.
  2. Fortunately, operation is a char which is integer compatible.
  3. We need to test operation (a rather simple expression) to see if it's equal to the literals '+', '-', '*', or '/'.

You're set to go.

Do this...
Write the second step of our algorithm in applySwitch() using a switch statement.

Use the equivalent multi-branch if and switch statements above to help you transliterate the code from applyIf().

Do this...
Change apply() so that it now calls applySwitch() instead of applyIf(). Then uncomment the last assert in CalculatorTest#assertApplies(), compile, and run for a green bar. Even run the driver.

We originally proposed the switch statement as a more efficient solution than a multi-branch if. The switch is more efficient because the compiler generates code that jumps directly from the expression immediately (more or less) to the relevant case; none of the cases in between are dealt with. However, the only way the switch statement can be so much more efficient is because of all of the restrictions on it: drop-through behavior, integer-compatible, and equality tests with constants. Relax one of these restrictions and the switch won't execute any faster than the if.

This is a bit of a shame because the switch statement has a slightly better syntax than the multi-branch if: the cases are a little clearer and you don't need compound statements for all the statement lists.

Comments

Do this...
Of course, you've been doing this all along, but go back and make sure that all of the Javadoc comments you added are accurate.

Submit

Turn in the following things for this assignment:

Terminology

compound statement, control structure, drop-through behavior, else clause, fall-through behavior, functionally correct, integer-compatible issue, menu, menu driven, multi-branch if, multi-branch if, selective behavior, selective execution, statement block, switch statement, then clause