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
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
, 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 base
andexponent
yes parameters built-in Initialize result
to 1.0yes declaration built-in Set result
toresult
*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)
- Receive
base
andexponent
.- Initialize
result
to 1.0.- For each
count
from 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
choice
is 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 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 ifchoice
comes beforemyFirstChoice
orchoice
comes aftermyLastChoice
.
The operations here might use some explanation:
Operations Description Predefined? Name Library a char
"comes before" anotherchar
yes <
built-in a char
"comes after" anotherchar
yes >
built-in logical or yes ||
built-in
The nice thing is that char
s 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
operation
is the quit operation, exit the loop.- Read in two
double
values.- 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