Hands on Testing Java: Lab #8

Repetition Statements

Introduction

In the previous lab exercise, we explored the if and switch statements; the switch statement selects on an integer-compatible value while an if statement uses a boolean condition. Consider this code fragment:

if (operand2 == 0) {
  throw new ArithmeticException("Divide by zero attempted!");
} else {
  return operand1 / operand2;
}

This uses the condition operand2 == 0 to avoid a divide-by-zero error: if the condition is true, then the statement before the else is executed; otherwise, the statement following the else is executed.

Condition are also used to execute certain statements over and over again. These repetition statements use the condition to control the number of repetitions.

Java provides three looping statements that we'll use in four different ways: the while loop, the do loop, the for loop, and what we call the forever loop.

Today's lab exercise is to make a calculator program that has more functionality (i.e., the exponentiation operation) and is more user-friendly than the one you wrote in the previous lab exercise. You'll add a new computation, and you'll increase the user-friendliness of your driver.

Getting Started

Do this...

Create Javadoc comments for these classes and their members (variables, constructors, and methods). Some of these have already been done for you.

Do this...
Fix the compilation errors in CalculatorTest by replacing the ???s with good values. All of the code should then compile successfully. All of the tests but one will pass; the failed tests will involve the power function.

Exponentiation

Exponentiation is available in Java via the method Math.pow(). For this lab exercise (to learn about loops), you will not use Math.pow(), but you will implement the computation for yourself. Actually, your version will only work with integer exponents so it's not a perfect replacement.

To avoid confusion with Math.pow(), you'll call your method power(). This method will compute xn, i.e., x raised to the power n, where x is any real number and n is a non-negative integer.

When faced with a new problem, it's often helpful to solve a few examples by hand. For example, here's table with some of the powers of 2:

Method call Result
power(2,0) 20 = 1
power(2,1) 21 = 2
power(2,2) 22 = 2*2 = 4
power(2,3) 23 = 2*2*2 = 8
power(2,4) 24 = 2*2*2*2 = 16
power(2,5) 25 = 2*2*2*2*2 = 32

The exponent tells us how many factors there are of the base element. Concentrate on those words "how many". How do you know that the expressions above are correct? If you really wanted to prove it, you might count each of the factors, probably by writing the count above each factor:

1st 2nd 3rd 4th 5th
25 = 2 * 2 * 2 * 2 * 2

We're counting the number of factors. This is crucial in deciding what looping statement to use.

Computationally, we should also note that our initial value is 1: any base raised to the zero power is always 1.

So now we can move on to the behavior of this new method, designed with an internal perspective:

Behavior of Calculator#power(??)

I am a Calculator object. When I compute the exponentiation function, I receive a base value and an exponent value. I initialize the result to one. I count from 1 up to and including the exponent, and for each of these values I multiply the result by the base, putting this product back into the result. When this counting finishes, I return the result.

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

Objects of Calculator#power(??)
Description Type Kind Movement Name
The base double varying received base
The exponent int varying received exponent
A counter int varying local count
The result double varying returned result

We include count in this list because we need a variable to do the counting. It's not mentioned explicitly in the behavior paragraph, but this is one case where we have to read between the lines.

From this object list, we can specify the task of our method as follows:

Specification of Calculator#power(double,int):

receive: base, a double; exponent, an int.
return: result, a double.

Do this...
Go to the Calculator class, and add a method stub for the power() function which returns Double.NaN. Then uncomment the call to power in Calculator#apply(). Your code should compile fine.

Examine the method call you just uncommented in Calculator#apply(); in particular, consider this expression:

(int) operand2

This is known as a cast; it casts the double operand2 as an int because that's what the power() method expects. Without that cast the compiler would complain about operand2 being a double while the method expects an int.

Question #8.01 What error message do you get (verbatim) when you remove the (int)?
To be answered in Exercise Questions/lab08.txt.

Do this...
Put the (int) back.

From our behavioral description, we have the following operations:

Operations of Calculator#power(double,int)
Description Predefined? Name Library
Receive base and exponent yes parameters built-in
Initialize result to 1.0 yes declaration built-in
Set result to result*base yes *= built-in
Repeat multiplication, counting from 1 to exponent yes for built-in
Return result yes return built-in

We can organize these operations into the following algorithm:

Algorithm of Calculator.power(double,int)
  1. Receive base and exponent.
  2. Initialize result to 1.0.
  3. For each count from 1 to exponent:
        result *= base.
  4. Return result.

Testing the Power Method

The CalculatorTest class has test methods for testing the Calculator.apply() method in a variety of ways. You can use testApplyPower() to indirectly test the Calculator.power() method.

Do this...
Your code should compile successfully, and you can run this test-case class for a red bar.

Coding the Power Method

As we have seen before, the counting for loop is designed to implement counting behavior (as its name suggests). Its pattern for counting looks like this:

counting-for-loop pattern
for (int loopVar = firstValue; loopVar <= lastValue; loopVar++) {
  bodyStatements
}
  • loopVar is the loop-control variable.
  • firstValue and lastValue are the beginning and ending values for loopVar, respectively and inclusively.
  • bodyStatements is the list of statements that are executed repeatedly.
  • Execution: bodyStatements is repeatedly executed with loopVar set to different values between firstValue and lastValue, inclusive and in increasing order.
  • Warning: there's no semi-colon before the opening curly brace!

Do this...
Using this information, complete the definition of power(). Whatever you do, do not change the algorithm! (Hint: count is never used in the body of the loop!)

Spend some time looking at the counting-for-loop pattern, and compare it to the algorithm. In order to use the pattern for a counting for loop, you must figure out each of the components of that pattern. If you get stuck, determine what code should replace these components of the pattern:

loopVar      
firstValue      
lastValue      
bodyStatements      

Do this...
Implement the entire power() method. Compile, and run the test-case class for a green bar.

Do this...
You can (and should) also run the driver.

Characterizing Loops

The pattern for a general for loop has a little more flexibility:

general-for-loop pattern
for (initializationExpr; condition; stepExpr)
  statement
  • initializationExpr is any initialization expression, including a declaration.
  • condition is any boolean expression.
  • stepExpr is an arbitrary expression.
  • statement is a single statement, the body of the loop.

The counting for loop is really just one extremely common pattern needed in many algorithms. But there's nothing holding us back from changing the pattern. For example, if for some reason you wanted to count downwards and output the multiples of 12 from 100 to 1, then you could write this loop:

for (int i = 100; i >= 1; i--)
  theScreen.println(12 * i);

Such a loop continues to execute so long as the condition i >= 1 evaluates to true.

Condition Controlled

A condition controls a for loop just like a condition controls an if statement; all of the other Java loop statements are also controlled by conditions.

With all of these control-flow statements, the order in which things are executed is crucial. This seems pretty obvious in an if statement where we have to evaluate the condition first to see which code to execute. But sometimes we do want to execute the body of a loop before its condition. Java allows the programmer to choose from three possiblilites:

The for loop is a pretest loop, because it evaluates its condition before the loop's statement is executed.

Problem Dependent

However, the for loop is designed primarily for problems that involve counting through ranges of numbers where the range can be determined in advance. In the power() method, you know the exact range: 1 through exponent. There are many problems that do not fit this pattern.

The other loops are general-purpose loops, designed for problems where the number of repetitions is not known in advance. We have a general-purpose loop for each of the categories:

To see why this is significant, let's examine the problem of getting a menu-choice and ensuring that it is valid.

Getting a Valid Menu Choice

The program in CalculatorCLIDriver uses the method getMenuChoice() from the Menu class to get the user's menu choice. Getting a menu choice is a potential source of user error because the user could enter an invalid choice.

One way to handle such errors is to repeatedly display the menu and read in the user's choice so long as she continues to enter invalid menu choices. But how many times will the user enter in bad data? Well, good typists will never enter bad data; normal people will fail at least once or twice occasionally; the hopeless will almost continually enter bad data. Since no one can predict how many times someone will enter bad data, a counting for loop is inappropriate for this situation.

A general-purpose loop gives you a way to handle user errors, but you must decide which one to use. Begin by writing a partial algorithm for this problem using a generic Loop statement where you don't worry (for the moment) about the condition that controls the loop:

  1. Loop:
    1. Display menu.
    2. Read choice.
    End loop.
  2. Return choice.

Every loop has a continuation condition that defines the circumstances under which you want repetition to continue; every loop also has a termination condition that defines the circumstances under which repetition should terminate. These two conditions should be the logical negation of each other, and you'll need just one of them to actually control a particular loop.

To determine how control should leave the loop, you must identify these conditions. For this particular problem, you have these conditions:

Continuation Condition

Termination Conditions

choice is an invalid menu choice.

choice is a valid menu choice.

Since choice is not known until after its been read in, that would be the logical place to exit the loop:

Algorithm of Menu#getMenuChoice()
  1. Loop:
    1. Display menu.
    2. Read choice.
    3. If choice is a valid menu choice, exit the loop.
    End loop.
  2. Return choice.

In this algorithm, the controlling condition is evaluated at the bottom of the loop, which implies that a post-test loop is the appropriate loop to choose.

As noted above, the post-test loop is the do loop, and its pattern is as follows:

do-loop pattern
do {
 bodyStatements
} while (condition);
  • bodyStatements are the statements to be executed in the loop.
  • condition is the Boolean expression that determines when the loop continues.

When execution reaches such a loop, the following actions occur:

  1. bodyStatements execute.
  2. condition is evaluated.
  3. If condition is true, go back to step 1; otherwise, continue with the statement after the do loop.

Since its condition is not evaluated until after the first execution of bodyStatements, the do loop guarantees that bodyStatements will be executed one or more times. For this reason, the do loop is said to exhibit one-trip behavior---the program takes at least one trip through the loop's body.

Since repetition continues so long as the condition is true, a do loop uses a continuation condition. For this current problem, you want to continue this loop if the input is invalid.

There are a variety of ways that one can indicate what menu choices are valid. As written for this lab, the code already implements a menu that uses consecutive letters (i.e., 'a', 'b', 'c', 'd', and 'e'). This was changed from the previous lab to make the notion of a "valid menu choice" easier for this lab. All you have to do is check if the input is within this range of letters.

Take a look at the Menu class. There's nothing in this class that indicates what range of characters are valid. (We recognize the range 'a' through 'e' unfairly because we can read the code in CalculatorCLIDriver.) So in addition to storing away the text of the menu, a Menu object must also store away the range of characters that are valid menu items. Sounds like new attributes!

Attributes of Menu
Description Type Name
The text of the menu String myMenu
The first valid character char myFirstChoice
The last valid character char myLastChoice

Do this...
Declare and Javadoc the two new attributes as instance variables in the Menu class.

This will also require a change to the Menu constructor.

Do this...
Add two parameters to the explicit-value constructor, and initialize the two new instance variables appropriately.

Helpful hint: The Menu class should not know anything about 'a' or 'e'; it uses its instance variables. CalculatorCLIDriver#createMenu() worries about these specific values which it gives to theMenu through the Menu constructor.

The driver now needs to change because the constructor for the Menu class has changed. Look at the call to the explicit-value constructor of Menu in the helper method CalculatorCLIDriver#createMenu(). It's only getting one argument, but you've added two more parameters. For the menu that the driver offers up, your first valid choice is the letter 'a', the last valid choice is 'e'.

Do this...
Modify the call to the Menu constructor in CalculatorDriver#createMenu() to include these two new arguments (in addition to the existing argument). The code should successfully compile now.

Now in Menu#getMenuChoice(), since myFirstChoice and myLastChoice define the range of valid choices, you can express the loop's continuation condition (i.e., the choice is bad) in terms of those values:

choice is invalid if and only if choice comes before myFirstChoice or choice comes after myLastChoice.

The operations here might use some explanation:

Operations
Description Predefined? Name Library
a char "comes before" another char yes < built-in
a char "comes after" another char yes > built-in
logical or yes || built-in

The nice thing is that chars can be treated like integers in Java, so the comparison operators work just fine on them, and they (usually, at least) do the alphabetical ordering that you'd expect.

Do this...
Follow the algorithm above for Menu#getMenuChoice() to rewrite the code with a do loop; use the comparison above as the condition of this do loop.

It is necessary to declare choice outside the loop; if declared within the loop, choice would be local to the loop, and then we could not use it in the return statement since it's outside the loop.

Do this...
Compile, and run the test case for a green bar (since no computations changed), and run the driver to test out this new feature. Be sure you enter some bad as well as good input for the menu choice because your test-case class is not testing this feature. You have to do it by hand.

Getting Valid Numeric Input

Another potential source of error occurs when our program reads values for operand1 and operand2 from the keyboard; the user might enter some non-numeric value. The code for reading in an operand is encapsulated in the CalculatorCLIDriver#inputOperand(int).

Presently this method prompts for an operand and then reads in a double. When bad input is entered, the program crashes. But to fix up this method, we need to look at exceptions and at a new loop.

Exceptions

In Java an error is indicated by an exception which is thrown. The method Keyboard#readDouble() throws a NumberFormatException whenever it cannot turn its input into a double. For example, if the user were to enter howdy! for one of their double values, parseDouble() will throw the exception.

Keep in mind that everything in Java is an object, and exceptions are no different. An exception is an object just like a String or a Screen or a Calculator. The only difference between exception objects and "normal" objects is that exceptions are thrown and caught instead of returned and received.

When an exception is thrown, it can be caught. If we don't catch the exception, the Java Virtual Machine deals with it by printing an error message (with some helpful debugging information) and quitting the whole program. If our code catches the exception, it can deal with it however it feels is appropriate (quitting the whole program is pretty drastic!).

To catch an exception we need to wrap the code that may fail in a try-catch block. In its simplest form the try-catch has the following form:

try-catch-statement pattern
try {
  tryStatements
} catch (ExceptionType e) {
  exceptionStatements
}
  • The tryStatements are executed first.
  • If they execute without any exceptions being thrown, then exceptionStatements are skipped.
  • If some statement in the tryStatements throws an exception, the rest of the tryStatements are skipped.
    • If the thrown exception is of type ExceptionType, then the exceptionStatements are executed with e (a variable) set to be the exception object.
    • Otherwise, the exception is thrown to the next try-catch statement.
  • The catch clause can be repeated for other exception types

Think of (ExceptionType  e) as being analogous to a parameter list of a method except that you're required/allowed to catch exactly one exception. You can then use the exception object in e (the conventional name for this variable) to generating some debugging information or feedback to your user.

By writing several catch clauses, you can pick which exceptions you want to handle; whatever exceptions you do not catch will be the responsiblity of some other code.

You can use a try-catch to deal with a NumberFormatException.

For example, I might read in a person's age and handle problems like so:

try {
  theScreen.print("Enter your age: ");
  age = theKeyboard.readInt();
} catch (NumberFormatException e) {
  theScreen.println("ERROR: that's not an integer age!");
}

This is most effective in a loop, so let's get that in first.

Pretest Loops

Suppose you wished to give the user another chance to enter valid values, instead of just terminating the program. Clearly you need to have a loop that will continue executing until you get two good values.

This time you'll use a pretest while loop:

while-loop pattern
while (condition)
  bodyStatement
  • condition is any Boolean expression; it determines when the loop continues.
  • bodyStatement is either a single or compound statement.

When execution reaches this statement, the following actions occur:

  1. condition is evaluated.
  2. If condition evaluates to false, then bodyStatement is skipped, and control proceeds to the statement after the loop.
  3. Otherwise bodyStatement is executed, and execution returns to Step 1.

If the condition is false right at the beginning, it might never execute the body of the loop. Since it might never take a trip through the body of the loop, the while loop is said to exhibit zero-trip behavior---the program might make zero trips through the loop.

Design

Here's a list of operations we need to do in approximately the order we want to execute them:

One tricky consideration is that the program may fail on reading the first data value; however, the user may have already entered in more data (possibly for the second value or just by accident). To be fair to the user, this extra input should be discarded, and the user should re-enter the operand.

The first two tasks must be done at least once, and may have to be repeated if we get bad input. If we use a while loop, there are two choices. One choice is to do these steps once outside the loop and also inside the loop:

  1. Display a prompt.
  2. Read operand.
  3. Loop so long as operand is invalid:
    1. Print an error message.
    2. Discard unread characters.
    3. Display a prompt.
    4. Read operand.
    End loop.
  4. Return operand.

This does the job, but at the cost of using two statements twice (once: steps 1 and 2; twice: steps 3c and 3d). Programmers are bothered by duplicated code like this.

Another option is to use a Boolean flag to control the loop. Let's try this option. We will use a boolean value called badInput to control the loop:

  1. Set badInput to true.
  2. Loop so long as badInput is true:
    1. Display a prompt.
    2. Read operand.
    3. Set badInput to false.
    End loop.

This works for good input, and it only "costs" us an extra variable.

But what happens when the input fails? As discussed above, an exception is thrown. So we have to try to read in the operand, and then catch the problems. In our list of operations above, we even have operations that must be executed when the input fails.

Our modified algorithm is:

  1. Set badInput to true.
  2. Loop so long as badInput is true:
    1. Try to execute:
      1. Display a prompt.
      2. Read operand.
      3. Set badInput to false.
      Catch the number format exception
      1. Display an error message to the screen.
      2. Discard unread characters.
    End loop.
  3. Return operand.

Remember that when an exception is thrown (by the "Read operand." statement in this case), control goes directly to the appropriate catch statement; the rest of the try is skipped. So badInput is set to false only when the input is good.

Do this...
Modify inputOperand() to implement this last algorithm. Compile, and run the driver, fully testing by hand this new error checking feature.

A few observations about the code that you add:

One Execution, Multiple Calculations

Your driver implements basically this algorithm:

  1. Display a greeting.
  2. Display a menu of operations and read operation, guaranteed to be a valid menu operation.
  3. Read in two double values.
  4. Compute and display result.

The last two steps here are gross simplifications of the actual code, but a little simplicity here will make our discussion a little easier.

Using this algorithm, you have to rerun our program for every calculation, which is inconvenient for the user. A more convenient driver would wrap some of the statements in a loop, so that the user could perform multiple calculations without having to rerun the program. To do so, you might modify the algorithm like so:

  1. Display a greeting.
  2. Loop:
    1. Display a menu of operations and read operation, guaranteed to be a valid menu operation.
    2. Read in two double values.
    3. Compute and display result.
    End loop.

This is missing a condition that tells the loop when to terminate or continue. One way to have the user indicate that she wants to quit is to add quitting as an available operation in our menu.

Do this...
Add an additional menu choice (i.e., 'f') to the menu string for the user to select when she wants to quit. Change the call to the Menu constructor in CalculatorCLIDriver#main() since 'f' is the last valid value instead of 'e'.

The termination condition is quite simple; the loop should quit when operation == 'f'.

So when to check this condition? The ideal place is to evaluate it as soon as the program knows it. If we step line by line through the algorithm, this doesn't happen until just after operation is read in with getMenuChoice().

If we look at it from the bottom of the algorithm, this makes sense. Why ask the user to enter in the operands if she wants to quit? Why do any computations if she wants to quit? So clearly those statements should not be executed.

Therefore, testing for user-quit should take place right after operation is read in, before the operands are read in. This puts the test in the middle of the loop:

Algorithm of CalculatorCLIDriver#main()
  1. Display a greeting.
  2. Loop:
    1. Display a menu of operations and read operation, guaranteed to be a valid menu operation.
    2. If operation is the quit operation, exit the loop.
    3. Read in two double values.
    4. Compute and display result.
    End loop.

Java provides us with two loops to implement an unrestricted, forever loop like this:

unrestricted-loop pattern

while version

for version

while (true) {
  statementList1
  if (condition)
    break;
  statementList2
}
for (;;) {
  statementList1
  if (condition)
    break;
  statementList2
}
  • Either form can be used (Java convention is to use the while version).
  • statementList1 is guaranteed to be executed at least once.
  • condition determines when the loop terminates.
  • statementList2 is executed when condition is false, and may never be executed even once.

The for loop version is a carry over from C++; while C++ programmers can (now) use the while version, they still favor the for version. Java programmers, for whatever reason, favor the while version.

In either case, the conditions in the loop controls (true in the while loop, empty in the for loop) always evaluates to true, so the body is always and continually executed. Consequently, the loops are also known as infinite loops.

Generally we do not want an infinite number of repetitions, and certainly not for our current problem. As shown in the pattern above, this is accomplished by placing an if statement that uses a termination condition to trigger a break statement. When a break statement is executed, execution is immediately transferred out of the forever loop to the first statement following the loop.

Do this...
Modify your source program to implement the new algorithm, and then compile, and run the driver to test the newly added statements.

Be careful when modifying this code:

Submit

Terminology

Boolean flag, cast, catch an exception, continuation condition, counting for loop, do loop, exception, forever loop, for loop, for loop, general-purpose loop, generic Loop statement, infinite loop, one-trip behavior, post-test loop, pretest loop, repetition statements, termination condition, throw an exception, unrestricted loop, while loop, zero-trip behavior