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.
Do this...
edu.institution
.username
.hotj.lab07
Exercise Questions/lab07.txt
.Design/lab07.txt
.Calculator
.CalculatorTest
.CalculatorCLIDriver
.Add the appropriate Javadoc comments to these files.
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.
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.
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.
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() {
|
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
andoperand2
, twodouble
values.
return: adouble
.
Do this...
Write the three method stubs to satisfy the compiler. Return
Double.NaN
.
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
andoperand2
, twodouble
values.
return: the real-number result of applyingoperation
tooperand1
andoperand2
.
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
andoperand2
yes +
built-in Compute the difference of operand1
andoperand2
yes -
built-in Compute the product of operand1
andoperand2
yes *
built-in Compute the quotient of operand1
andoperand2
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()
inCalculator
- Receive
operation
,operand1
andoperand2
.- If
operation
is '+':Otherwise, if
- Set
result
to beoperand1
+operand2
.operation
is '-':Otherwise, if
- Set
result
to beoperand1
-operand2
.operation
is '*':Otherwise, if
- Set
result
to beoperand1
*operand2
.operation
is '/':Otherwise,
- Set
result
to beoperand1
/operand2
.End if.
- Complain that
operation
is invalid.- 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.
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.
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.
if
StatementSelective 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 (
|
two-branch-if-statement pattern |
if (
|
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 if
s, we end up with a
multi-branch if
:
multi-branch-if-statement pattern |
if (
|
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.
switch
StatementFrom 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:
operation == '+'
) is evaluated.operation ==
'+'
followed by operation == '-'
) are
evaluated.operation ==
'+'
followed by operation == '-'
followed by
operation == '*'
) must be evaluated.operation ==
'+'
followed by operation == '-'
followed by
operation == '*'
followed by operation ==
'/'
) must be evaluated.operation ==
'+'
followed by operation == '-'
followed by
operation == '*'
followed by operation ==
'/'
) must be evaluated.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 (
|
When execution reaches a switch
statement, the
following actions occur:
expression
is evaluated.CONSTANTi
s,
then execution begins in statementListi
and proceeds through every statementListj
(for j > i) until the very end of the
switch
statement.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.
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:
break
is a
keyword, and as a statement you just add a semi-colon:
break;This is perhaps the most common way to end a statement list in a
switch
. This breaks you out of the switch
statement.return
statement. Since the return
statement stops the method, it must, of course, stop the
switch
statement in the method.return
statement, the throw
statement
immediately ends the method which must also end the
switch
. As in the past, you throw an exception when
there's a problem.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.
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 CONSTANTS
s
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
int
s and char
s 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.
A switch
statement tests only equality.
These two statements are equivalent to each other:
Multi-branch |
|
---|---|
|
switch ( |
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.
So, abusing the superlative, that's three most-important things
you have to remember about the switch
statement:
statementListi
should end with break
, return
, or
throw
.Let's bring this all back to your code for
applySwitch()
.
break
in each
statement list of the switch
.operation
is a
char
which is integer compatible.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 case
s 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.
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.
Turn in the following things for this assignment:
apply()
should call
applySwitch()
.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