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.
Do this...
edu.institution.username.hotj.lab08
Exercise Questions/lab08.txt.Design/lab08.txt.Calculator.CalculatorTest.CalculatorCLIDriver.Menu.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 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
Calculatorobject. 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 doublevarying received baseThe exponent intvarying received exponentA counter intvarying local countThe result doublevarying 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, adouble;exponent, anint.
return:result, adouble.
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 baseandexponentyes parameters built-in Initialize resultto 1.0yes declaration built-in Set resulttoresult*baseyes *=built-in Repeat multiplication, counting from 1 to exponentyes forbuilt-in Return resultyes returnbuilt-in
We can organize these operations into the following algorithm:
Algorithm of Calculator.power(double,int)
- Receive
baseandexponent.- Initialize
resultto 1.0.- For each
countfrom 1 toexponent:
result*=base.
- Return
result.
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.
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
|
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.
The pattern for a general for loop
has a little more flexibility:
| general-for-loop pattern |
for (
|
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.
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.
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:
while loop provides a general pretest
loop.do loop provides a general post-test
loop.To see why this is significant, let's examine the problem of getting a menu-choice and ensuring that it is valid.
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:
menu.choice.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 |
|---|---|
|
|
|
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()
- Loop:
End loop.
- Display
menu.- Read
choice.- If
choiceis a valid menu choice, exit the loop.- 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 {
|
When execution reaches such a loop, the following actions occur:
bodyStatements
execute.condition is
evaluated.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 MenuDescription Type Name The text of the menu StringmyMenuThe first valid character charmyFirstChoiceThe last valid character charmyLastChoice
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:
choiceis invalid if and only ifchoicecomes beforemyFirstChoiceorchoicecomes aftermyLastChoice.
The operations here might use some explanation:
Operations Description Predefined? Name Library a char"comes before" anothercharyes <built-in a char"comes after" anothercharyes >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.
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.
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 {
|
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.
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 (
|
When execution reaches this statement, the following actions occur:
condition is
evaluated.condition
evaluates to false, then bodyStatement
is skipped, and control proceeds to the statement after the
loop.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.
Here's a list of operations we need to do in approximately the order we want to execute them:
operand.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:
operand.operand is invalid:
operand.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:
badInput to true.badInput is
true:
operand.badInput to false.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:
badInput to true.badInput is true:
operand.badInput to false.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:
boolean expression or
variable with (b==true)
or (b==false),
where b is the
boolean expression. This seems to be a tempting thing
to write at first, but dropping the explicit test actually makes
the code more readable. For example, if I had a
boolean value named isProblem, I'd write
this if statement:
if (isProblem)
throw new SomeKindOfException("You dummy!");
I use the variable directly, not (isProblem ==
true).theKeyboard.readLine();Technically this returns a value, but since we think it's junk anyway, there's no point in putting the value into a variable which will never be used.
operand
before the loop (preferably before you declare and
initialize badInput). To make the compiler
happy, you must also initialize the variable to be some default
value, like 0.0. We know that they will get a
value from the keyboard, but the compiler cannot be as sure.Your driver implements basically this algorithm:
operation, guaranteed to be a valid menu
operation.double values.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:
operation, guaranteed to be a valid menu
operation.double values.result.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()
- Display a greeting.
- Loop:
End loop.
- Display a menu of operations and read
operation, guaranteed to be a valid menu operation.- If
operationis the quit operation, exit the loop.- Read in two
doublevalues.- Compute and display
result.
Java provides us with two loops to implement an unrestricted, forever loop like this:
| unrestricted-loop pattern | ||||
|
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:
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