| ||||
Top-Down Design | ||||
|
Last lecture we showed some simple programs which manipulate numbers.
In the first part of this lecture we will go through a somewhat longer example,
to demonstrate some basic ideas about how to approach writing a program.
| ||||
| A Program To Solve Quadratic Equations |
Let's suppose we want to write a program to solve quadratic (i.e. second-order) equations.
As you know, given a quadratic equation of the form:
The quadratic formula gives the two roots as:
and
Now, how do we write the program? As we said in the first lecture you shouldn't sit down at the computer until a program is thoroughly designed. Instead, you develop the program by beginning with a very abstract, higher-level description of the solution and refining it over an over, filling in details with each round of refinement. This methodology is known as Top-down design.
| |||
| A First Sketch |
In this example, we could begin with a very simple sketch of the program's behavior:
Now, in fact, this is really the basic outline that almost all computational programs are going to start with. They are almost all of the form:
Anyway, now we can take each of those steps and refine them a step further. Keeping in mind the analogy that a program is alot like a recipe, as we refine the parts of the problem we should keep track of the values that need to be stored and any special routines that need to be used, much as the recipe author would keep track of ingredients and tools.
| |||
| Filling In The Details |
How can we refine the first step? Well, there are three coefficients, and we need to prompt for them
and get their values from the user. Now, there are actually two ways to go here. We could either
expand that step as:
2. Get the coefficients of the equation from the user or we could expand it as:
2. Get the coefficients of the equation from the user This is just a stylistic choice, and either one seems fine for this example, since the problem specification (which was very higher-level) did not specify one choice or the other. We'll go with the first one for now as it will save a little typing.
Has this refinement told us anything about our ingredient and tool lists?
Yes, we need to store the 3 coefficients.
This leads to the question of what kind of variable we should use?
The problem didn't specify, so we have to make a choice.
Integer coefficients would work for alot of uses. But there is no reason to think a user
might not want to have floating point coefficients. So, to make the program most general,
At this point we could either continue by refining these two new steps to greater detail, or put them aside and continue with the second higher-level step. As it happens, I'd say that the input step is probably already refined enough to be programmed without difficulty, so let's just continue to the computation step, the heart of the program. Our first inclination might be to write something like:
2. Solve the quadratic formula given those coefficients That would be a perfectly good solution. But you should notice that the two computations share a fairly hard-to-compute piece: .
2. Solve the quadratic formula given those coefficients
Did this add anything to our lists of ingredients and tools?
Yes, we need to store the value of the subexpression
and of the roots. Also, we will need to use the
| |||
| The Complete Design |
The third step is probably already refined enough, so let's leave it alone. The whole design is
then:
| |||
| The Finished Program |
Each of these steps is now small enough to translate directly into a Java statement with almost no
thought at all. That means the design is refined enough for youto head to the computer and start writing the actual program.
If, while you are programming, you find yourself puzzling over how to implement
a step in your design, then it isn't refined enough. Step back from the computer and
look at that part of the design again.
Think about it as a general task, not as a Java program. If you had to
teach someone who didn't know how to perform this task (how to do it, not how to program it),
how would you break it down into steps for them?
How far you have to break down the program's design depends on how comfortable you are
converting small tasks to programs, and how familiar you are with a particular task.
For example, if there were no square root method in the In this case, as we said, though, it is an easy jump from the design above to the following program. Note that we have included the steps of the design specification as comments in the program. This is a good place to start when commenting your programs.
| |||
| Brittle Programs |
The program we just wrote is pretty good up to a point, but it has some failings. In particular
it is what we call brittle. In a brittle program, if the input isn't just right
the program will
bomb or produce meaningless output. For example, what happens if we give this program the value
0 for coefficient a? The program tries to compute the value
of the roots using the lines:
| |||
| Not A Number |
In many languages, excecuting these statements would cause the program to bomb out
with a division by zero error, or something similar. In Java (and some implementations
of other languages) the result is a bit curious. For example, suppose we enter 0, 1, and 2
for the coefficients. The program does not bomb, but it produces the output:
That's because the first root is (0.0 / 0.0), which is mathematically undefined, or Not a Number. The second root is (-2.0 / 0.0), which is Negative Infinity. You see, Java folows an IEEE specification for floating point arithmetic which requires that it properly handle undefined and infinite results of computations. This is useful since when such results occur in a subexpression of a larger computation, they may not matter. For example, 0.0 divided by anything other than 0.0 is 0.0, so in the expression (0.0 / (1.0 / 0.0)) it doesn't matter that the subexpression (1.0 / 0.0) is infinity. The overall result is still 0.0.
| |||
You should note that in Java this behavior only occurs for
floating point arithmetic. Had we written the program using integer variables for the
coefficients, then, if we had given it the coefficients 0, 1, and 2, it would have failed
with the error:
| ||||
| Runtime Error Messages |
This error message tells you that the program has halted because of an arithmetic exception
(a fancy word for error), in particular an attempt to divide by zero, in the method main
of the class quadratic, on line 29 of the original source file.
One of the nice things about Java is that
when your program is compiled to bytecode
it maintains enough information about your source program so that it can tell you exactly
which line of your program generated an error, even with a runtime error (that is, an error which occurs due to some condition arising when the program is executed that the design of the program did not prepare it for.) Most programming language
systems can only tell you the precise location of errors that occur at compile time.
| |||
Conditional Execution | ||||
|
In any case, even though the floating point math in the last example is technically correct,
the program's answer is still wrong over all. This equation does have a defined root, you just
can't find it using the quadratic formula (since it is really a linear equation at that point). It would be nice to have the program respond that the
problem is outside its domain, rather than have it give back an incorrect and meaningless
answer in this case. So far, the flow of control in our programs has been strictly downward, executing one statement after another. Solving this problem will require the program to be able to to distinguish between good input and bad input and change the flow of control to choose the correct action in either situation.
| ||||
| The Conditional Statement |
For this purpose, Java (and every other programming language) provides
what is called the conditional statement, or if statement.
The conditional statement allows a program to test whether
some condition is true, and do one thing if it is true and do another if it is not. After the
program has finished doing the things it is supposed to for the
appropriate case, the flow of control returns back to its
original downward path. The grammar of an if statement in Java is:
Note that the test expression, which determines the behavior of the statement, must be enclosed in parentheses. The list of statements to execute in each case is enclosed in a pair of braces, and is called a block. A block can be as long or as short as necessary. It can even include its own variable declarations. Any variables declared in a block are only accessible within the block. If you try to refer to them elsewhere you'll get a compiler error. The following simple program asks the user to enter an integer and then uses a conditional statement to tell them whether the number is less than 10, or not:
| |||
| A More Robust Quadratic Solver | In order to make our quadratic equation solving program behave better, we should go back to the top-down-design and modify it so that, before attempting to solve the equation, the program checks the value of the first coefficient. If the first coefficient is not zero, the computation should proceed. Otherwise, an error should be printed and the program should terminate. In this design the second and third outer steps of the original design become subordinate to a check of the first coefficient. The new design is:
This new design yields the following Java program:
| |||
| Relational Operators |
The test expression used to determine which branch of a conditional statement to
execute can be any Java expression which evaluates
to a boolean value. Most commonly it will involve comparing
one value or expression with another using a relational operator.
In this example we used the not-equals operator,
| |||
|
Note that the equality operator is typed with two equal signs.
This is to distinguish it from the assignment
operator. If you use the assignment operator by mistake the Java compiler
will generally catch and report your error.
This is one of the places where Java improves upon C++.
In C++ this is not necessarily an error.
However, even though there are some valid uses of this expression, it is a typographical
error most of the time. Because the compiler won't detect it,
it is one of the most common, and hardest to debug, errors that C++ programmers make.
(This is a bit of an oversimplication of the situation in Java. As we said, there are still some
situations where the error can be made in Java and not caught. For a more complete
explanation, click here.)
| ||||
| When There Is Only One Case |
Sometimes you may
have a block of code that you wish to execute when some condition holds, but no
particular alternative task to do if the condition doesn't hold. In that case, the else
and its block are simply left out. The syntax of this limited form of the conditional is:
While it is legal to have an empty block,
it is generally bad style. If the should be rewritten as:
| |||
| Another Example |
Suppose we want to write a program to test whether a person is
legally old enough to drink alcohol. Such a program could speed up the entrance lines
at a five-college party and might come in handy some time.
The program should ask for the year the person was born and check
whether it has been 21 years since then or not.
If the test succeeds, the program should print a message saying the person can
drink. If the test fails, it should print a message saying the person is too young.
The following design follows naturally from that specification:
This design becomes the following Java program:
| |||
| A Better Design |
Unfortunately, a little testing (or, God forbid, thought) shows that this program is too simple.
It wil allow people who were born late in
1977 to drink, even though they are not quite legal yet. The solution is to check the month they were born as well. Since this is only needed when the person was born in 1977, we break the test for how many years old the person is into cases for more than 21, exactly 21, and less than 21. Then we will put the month-based test inside the statements that get executed when the year test comes out to 21. This way we won't bother asking people for the month they were born if it won't make a difference. Of course, this design has the same sort of bug as the last one, but here it comes down to the day a person born in September 1977 was born. We'll ignore that problem, and pretend this is an adequate solution. The new design is:
| |||
| Nested Conditional Statements |
The design for the new program calls for taking one of the cases and breaking it down
into sub-cases. This will require putting one conditional statement inside the block
for another, which is called a nested conditional. But there really is nothing
special about it, since a conditional statement is just a kind of statement. So don't
worry about it, just do it. The program that corresponds to this design is:
| |||
| Mutually Exclusive Cases with else-if |
The program above calls for breaking up the situation into three mutually exclusive
cases, depending on the year the person was born. We accomplished this by
nesting successive cases in the else part of the previous conditional.
The overall structure
was:
Notice that the block for the outer
In general, we recommend always including the braces as it
makes it easier to add statements to that case later on if you decide you need to.
However, in this particular situation, where the goal is to create
a series of mutually exclusive cases, it is unlikely that we will ever want to add more statements in the For example, in this case we would write:
This can be extended as many steps as we want. If we want to test which decade of life someone is in, we could write:
| |||
| Overlapping Cases |
It is important to remember that in the last example each successive if
is really nested in the else block of the preceeding one.
This is what makes the cases mutually exclusive.
| |||
If we left out the elses then these would become several
disconnected statements, that would be run in order. That is, if
we wrote:
then if the value of ageInYears were 25 the
program would print both In their twenties. and
In their Thirties., since each of these one-case
conditionals is a separate statement, and two of them will
have their test expresssions evaluate to true.Even when the cases appear mutually-exclusive, if the conditionals are written as independent statements then you could have multiple cases execute. For instance, if we wrote:
It would certainly seem that only one of the three blocks
could be triggered. However, suppose the value of testValue
were initially 7, then the code in the first if's
block would execute. If this code included an assignment that changed the
value of testValue to be 20, then the third if's
block would execute as well.
Therefore, if you intend for cases to be mutually exclusive, you should
generally use the if-else construct (or the
| ||||
Complex Tests | ||||
Often the condition for determining which branch of a conditional to execute
will be more complex than you can test using a single relational expression.
For example, you might want to test whether the value of an int variable x
is between one and ten, inclusive. Using the habits you have learned in years of
math classes, you might try to write something like:
Unfortunately, if you do you will get a compiler error like:
| ||||
The reason for this is that, to Java, the relational operators are much like the
arithmetic operators, and the expression (1 <= x <= 10)
isn't all that different from the expression (1 + x + 10).
To evaluate the overall expression the system must choose some order in which to evaluate
each operator. In this case, since there are two <= operators,
it just evaluates them left to right. Thus, it is as though you had typed
((1 <= x) <= 10).
Now, if
| ||||
| Using Nested Conditionals |
So, how do we perform this test? One choice is to use a nested conditional:
| |||
| Using Boolean Operators |
This can get pretty cumbersome, though, and doesn't work for all cases. Instead, Java provides
boolean operators, or logical operators,
that are used to logically combine the results of separate tests.
The boolean operators are &&, for "and", ||, for "or", and !, for "not". So, the test above can be written:
If you wanted to know if x is outside the range one to ten, you could write:
which is true if x is less than one or if it is greater than ten.
You could also write this test by reversing the sense of the original test using "not":
While I have used parentheses above to make the tests clearer (and you are encouraged to
do the same), they are not always strictly necessary. The
relational operators all have higher precedence than the boolean operators, so there
would have been no ambiguity without the inner parentheses in the first two examples:
the relational operators would have been computed first, and the boolean operator then used
to combine the two truth values.
Among the boolean operators,
| |||
| Short Circuiting Boolean Evaluation |
The boolean operators described above do what is called
short-circuiting when they are evaluated. What does this mean? Consider the following test on two integer variables:
"Ifwhich is equivalent to the code: then if x were zero and the boolean operator did not do short circuiting
we would have a problem:
as with the arithmetic operators, both arguments would be evaluated before they were combined.
Thus, even if x were zero, the computer would still compute y/x and attempt to compare it to 3. But the division by zero would cause the program to bomb.
Short-circuiting is based on the observation that if the first of two tests joined by an "and"
evaluates to
Java also provides "ordinary" boolean operators that always evaluate both their arguments
and do not short-circuit. These versions are written
| |||
Comparing | ||||
The relational operators are designed to be used on any numerical values (floating point or integer),
and can also be used to compare two values of type char. In addition, it is acceptable
to compare a pair of boolean values for equality/inequality, though not relative size.
| ||||
Unfortunately, the relational operators are not useful for testing the relationship between two
String
values. The relative size operators are not defined for strings, and while the ==
and != operators are defined, they will not behave as expected. In particular,
== will return true only if the two Strings
are really the same object; that is,
if they are in the same location in memory.
| ||||
For example, if you run the following program:
it will tell you that only the first pair of For this reason, Java provides a separate set of methods for comparing strings. As we said earlier, strings are really objects in Java, and these methods will be your first example of sending a message to an object you created (as opposed to the i/o objects which you simply import).
| ||||
The equals Method |
Every String is prepared to respond to the message equals with a
second String sent as the parameter. The String you send the message to will
respond back whether it and the String you sent as an argument contain the same series
of characters. So, for example, the following program, which replaces the use of ==
with calls to the equals method, will print that both pairs of String variables
are the same.
| |||
| Ignoring Character Case in Comparisons |
The comparison done by the equals method is case sensitive. Thus Hello
and hello would not be considered equal. If you wish the comparison to ignore case, use
the method equalsIgnoreCase, as in:
Alternatively, you can use the the toUpperCase
or toLowerCase methods to
first get copies of the strings that are all uppercase or all lowercase before comparing them, as in:
| |||
| General Comparisons |
If you want to know not only if two strings are equal, but, if they are not, which is
alphabeticaly larger, you use the compareTo method. If the String you send
the message to is less than the String you send as an argument then the result of the
call will be a negative integer; if it is larger then the result will be a positive integer;
and if they are equal then the result will be 0.
This test is case sensitive, with uppercase letters considered smaller than lowercase letters. There is no case-insensitive version. Therefore, if you want to do a case-insensitive general comparison, your only option would be to use
| |||
The Switch Statement | ||||
| Making A Selection Using Switch |
A situation that arises frequently in programming is having to select between several
options based on the exact value of a variable or expression. For example,
consider a program that manages a phone book and presents the user with the following list
(or menu) of five options for what to do at a certain point:
In order to handle the user's response to the menu with an if statement, the code
would look something like:
Java provides a special structure called the switch statement
to simplify this sort of construction. The syntax of a switch
statement is:
The first line gives the expression whose value is to be used to select among the
possible actions. This is followed by a series of labelled sections of code. The first section
whose label matches the value that the test expression evaluates to is executed.
So, the
| |||
Remember, the cases of the switch statement do not have to be in any particular
order. The evaluation rule is just that the first case whose label matches is executed. If no
label matches the expression's value, then none of the sections is executed.
Of course, from a stylistic standpoint, it is generally easier to read a program if there is
some natural ordering among the cases.
| ||||
Besides being a bit clearer visually, in general, the compiler will create code that executes faster
from a given switch statement than from an equivalent list of
if-else-if statements.
| ||||
Note that in this example we have used int values to label and select among
the various choices. Values of type char can also be used. This lets you
label a menu with characters instead of numbers if it is desirable.
| ||||
| Providing A Default Case |
Looking back at the phone book example, if the user types a value
other than 1 through 5 then none of the code sections are executed. However, it
would be nice to have the program print some sort of error in that case.
If we were using the block of which is executed if none of the previous labels have matched the test expression's value. So, in this case we would add the following code after the section for case 5::
| |||
| Falling Through To The Next Case |
When the break at the end of the section being executed is reached, execution jumps to the
code after the end of the switch statement. If the break
statement is omitted then when the end of the section is reached execution just
continues with the code in the next section. This is useful if one case's task is
really a superset of another case's. This can be carried to the extreme: if two
or more cases share the same task, just give a series of labels, followed by the
desired section. Both these ideas can be seen in the following template
| |||
Last modified August 28 for Fall 99 cs5 by fleck@cs.hmc.edu
This page copyright ©1998 by Joshua S. Hodas. It was built with Frontier on a Macintosh . Last rebuilt on Sun, Sep 6, 1998 at 5:44:14 PM. | |
http://www.cs.hmc.edu/~hodas/courses/cs5/week_04/lecture/lecture.html | |