As you likely found out the last time you purchased your last textbook, books can be quite expensive. One way that this expense can be reduced is through sharing: if you are unable to afford the full price for a book, and can find another person with the same problem, then you might purchase the book together, and share it. By having one person use the book and then another person reuse the book, the individual cost of using the book is cut in half.
A library is based on this idea of sharing. If a community of people pool their resources, then they can buy and share a centralized collection of books. By cooperating this way, each person has access to a greater set of books than he or she could afford individually.
Java implements shared libraries through its package system. You've already used libraries in packages. For example, in the previous lab exercise, the test-case class had this at the beginning:
import junit.framework.TestCase;
The effect of this directive is to tell the compiler that you
want to use the class TestCase
contained in the
package junit.framework
. Whenever you need a test-case
class, you can import this same class, reusing everything it
provides. Trust me: you do not want to have to write the
code in that class even once, let alone every time you
need it.
As another example, you've used these imports as well:
import ann.easyio.Keyboard; import ann.easyio.Screen;
These go to the ann.easyio
package, and you get
access to the Keyboard
and Screen
classes. Here again, you definitely do not want to have to type in
all of that code yourself for your programs. By using these classes
from the package, you're free to concentrate on your own
problem.
A Java package is thus similar to a library of books: it contains a set of classes ("books") that can be shared and reused by any program that needs them. It prevents programmers from reinventing the wheel.
There are two levels of libraries in Java:
Math
class is a great example of
this; all of the mathematical constants and computations are stored
in this class.java.lang
and
java.util
), Java also permits programmers to create
their own packages (like ann.easyio
and
junit.framework
). You've been creating packages since
the beginning of this manual, but that's mostly to keep each lab
separate from the others. Packages are more often organized by the
types of computations done.We have seen that Java provides a variety of classes, including
Math
that provides various mathematical methods, and
Character
that provides character-processing
methods.
To use a class, a program must use the import
directive to include the class:
import-statement pattern |
import
|
For example:
import java.lang.Math;
This would let your program use the Math
class.
However, you may recall that you never imported the
Math
class or even the java.lang
package.
This is because the java.lang
package and all of its
classes are automatically imported for us in every
program. This same package also provides us with the
String
and wrapper classes.
All other classes in other packages must be imported by your programs.
To call a method (invoke a method), you must name the class the method is in, name the method itself, and list out any arguments the method needs to do its job.
class-method-invocation pattern |
|
For example:
Math.pow(x, y)
The class is Math
, the method is pow
,
and x
and y
are the arguments.
When control reaches such a method call, the
arguments x
and y
are evaluated and their
values are passed to the pow()
method of the
Math
class. The method performs its task and (if
appropriate) returns a value back to its caller (in this case
x^y
).
The problem for this lab exercise will be to write some simple methods for computing English-units to metric-units, e.g., feet to meters. These conversions are obviously useful in a variety of places, so putting them into a library seems quite useful.
Do this...
edu.institution
.username
.hotj.lab04
Metric
.MetricTest
.lab04.txt
in your Exercise
Questions
folder.The first thing to decide is what measurement conversions should be defined. For example, the following are just a few of the useful conversions:
English Unit | Metric Unit | Conversion Formula |
---|---|---|
Inches | Centimeters | 1 inch = 2.54 cm |
Feet | Centimeters | 1 foot = 30.48 cm |
Feet | Meters | 1 foot = 0.3048 m |
Yards | Meters | 1 yard = 0.9144 m |
Miles | Kilometers | 1 mile = 1.609344 km |
Ounces | Grams | 1 ounce = 28.349523 g |
Pounds | Kilograms | 1 pound = 0.3732417 kg |
Tons | Kilograms | 1 ton = 907.18474 kg |
Pints | Liters | 1 pint = 0.473163 l |
Quarts | Liters | 1 quart = 0.946326 l |
Gallons | Liters | 1 gallon = 3.785306 l |
This lab exercise has you write a method to convert feet into meters. By storing this method in class, it can be shared by any and all programs that need to convert feet into meters, allowing them to avoid reinventing the wheel.
Software that is carefully designed is better than software that is poorly designed. This applies to any method, not just the program itself. Fortunately, the same OCD we use for a program works just as well for any method.
Behavior of Metric.feetToMeters()
The method should receive from its caller the number of feet to be converted. It should convert that quantity to meters by multiplying it by 0.3048, and return the resulting value to the caller.
Where a program typically inputs values from the keyboard, a method typically receives values from whatever code called the method. Similarly, where a program typically outputs values to the screen, a method typically returns a value to whatever code called the method.
This somewhat subtle difference is often ignored by beginning programmers, but it'll make your life easier if you drill this into your head now:
Helpful hint: Most methods do not interact with
the screen or keyboard. Receiving data is done with parameters
(not the keyboard); returning data is done with
the return
statement (not the
screen).
When you come up with the behavior of a method, you should also anticipate what could go wrong. In this case, there's really nothing that could go wrong. It is reasonable to talk about a negative number of feet: perhaps a positive number of feet refers to feet above sea level, zero feet means at sea level, and a negative number of feet refers to feet below sea level.
The objects of a method are similar to those of a program except that in addition to describing the type and kind of each object, we also want to describe its movement: does it move into the method from outside, does it move from the method out to the outside, or is it purely local to the method?
Objects of Metric.feetToMeters()
Description Type Kind Movement Name the number of feet double
varying in feet
the conversion factor double
literal local 0.3048 the corresponding number of meters double
varying out meters
This information provides us with what we need to specify the problem your method solves:
Specification:
receive:
feet
, the number of feet to be converted.
return:meters
, the equivalent number of meters.
Any data going in to the method is received; any data going out of the method is returned. This specification can be included in the documentation for the method.
This specification also provides us with what we need to write the method's signature, the first step in writing a method. A signature looks like this:
class-method-signature pattern |
public static
|
A precise method specification (derived from the object list) tell you how to build the signature.
double
). Thus, the method's
returnType
is double
.methodName
that reflects this: feetToMeters()
sounds pretty
good.feetToMeters()
must receive one real value,
the number of feet, from its caller. So feetToMeters()
should have one double
parameter, which we will call
feet
.Observe closely that the returned value is specified in the signature only by its data type. The parameter is specified by both its data type and a name. This is always the case in Java.
Do this...
Add the signature for feetToMeters()
to the
Metric
class now. Remember that everything
(except package and import statements) goes in the
class.
You will get errors about the signature because it's not compilable code yet. To make it compilable, turn it into a method stub:
class-method-stub pattern |
public static
|
Do this...
Compare this pattern to the pattern for a class method signature,
and turn the signature into a stub. Use Double.NaN
as
a dummy value.
The constant Double.NaN
stands for "not a number".
It is guaranteed that Double.NaN
is never
equal to any other double
, including itself!
This means that you're guaranteed to get a failure from your unit
testing when you use this stub. You'll know the method is being
tested, and so when you eventually get a green bar again, you'll
know that your method is working properly.
Continuing with the design, the list of needed operations is quite short:
Operations of Metric.feetToMeters()
Description Predefined? Name Library get a value from the caller yes parameter mechanism built-in multiply two real values yes *
built-in return a value to the caller yes return
built-in
The multiplication should be familiar from the previous lab. The other operations we'll explain as we code up the algorithm since they're particular to implementing a method.
We can then organize these operations and objects into the following algorithm:
Algorithm of Metric.feetToMeters()
- Receive
feet
from the caller.- Compute
meters
=feet
* 0.3048.- Return
meters
.
Now that the design is finished, we're ready to write our tests.
Wait a second. Testing? You haven't actually written the method yet. You have a signature, but it returns a dummy value. So why in the world would I have you write your tests next?
Well, as a matter of fact, some might argue that we've waited too long to write the tests; you should have written them before the design. This way you test the problem rather than the code.
So what's involved in the tests? First, you need a test method in the test-case class.
Do this...
MetricTest
is the test-case class, so open up this
class. Add this test method to the class:
public void testFeetToMeters() { }
This method declaration should make more sense; compare this
definition to the pattern for a method stub given above. The only
significant difference is the lack of the keyword
static
; this is due to the way that the JUnit library
works. It's actually very rare for a method to be static, but it
makes some other things easier to learn about methods.
Next we figure out what data we'd like to test. The problem is pretty simple, so there aren't really any really goofy cases to worry about. First figure out some interesting data:
The first three data inputs are simple inputs that should give us predictable results. The fourth input is a relatively random number I picked; I used a calculator to compute the equivalent number of meters. The last test tests negative inputs.
Here's the code for the first and third assertions:
assertEquals("0 feet is equivalent to 0 meters", 0, Metric.feetToMeters(0), 1e-3); // missing second... assertEquals("1/0.3048 feets is equivalent to 1 meter", 1, Metric.feetToMeters(1/0.3048), 1e-3); // missing fourth... // missing fifth...
Do this...
Add these tests to the test method.
I've given you the exact code for the first and third; use these examples to create the code for the other tests.
There's nothing that says I can't do some extra computation in
these tests, so note the argument passed to
feetToMeters()
in the third test. I could punch that
division into my calculator and use that result as the argument,
but computing an argument for the computation is okay.
However, computing the expected value probably wouldn't be a good idea; it allows too many things to go wrong.
The order in which you pass in arguments to
assertEquals()
is very important for debugging. When a
test fails, you'll be told that one value was expected but another
one was computed. The expected value---the value
you hope to get---should be first; the computed
value---the value you get from doing your
computation---should be second.
As we saw in the previous lab, testing double
s
requires an extra argument. This argument is a fudge
factor (or more formally, a tolerance);
arithmetic with double
s is inherently inaccurate. The
fudge factor allows the test to pass if difference between the
expected and computed values is less than this fudge factor. We're
using 1e-3
, which is 1x10-3 = .001 which
should give us enough accuracy for these computations.
To summarize, here's the pattern for a assertion of a
double
computation:
double-assert-equals-statement pattern |
assertEquals(
|
Do this...
Write the assert statements for the three other tests. Compile and run the test-case class; it
will red bar, but that's okay.
Do this...
Add a comment before the test method that explains what the method
is going to test.
Here's one possibility:
/** * This method tests the {@link Metric#feetToMeters(double)} method * with five different inputs. */
I've used a Javadoc format here. You're welcome to try your hand at running the javadoc tool, but it takes a bit of effort to get it working nicely. The important thing is that you have clearly stated what the test method actually tests. You don't have to go into specifics, but you should at least give anyone reading the comments a very good idea what the method does (not how it does it).
Now we're ready to code the method itself.
Do this...
Switch back to the Metric#feetToMeters(double)
method.
The general pattern for a method definition looks like this:
class-method-declaration pattern |
public static
|
A method definition (or declaration) is just like the stub, except the returned dummy value is replaced with useful computation statements.
Let's take the statements of the algorithm step by step:
feet
from the caller.This step is already taken care of for us by the Java
method-call mechanism. In the test-case class, we called
feetToMeters()
several times. Consider this call from
testFeetToMeters()
:
Metric.feetToMeters (1/0.3048)
The division will be computed first, and then a copy of that
value will automatically be stored in the parameter
feet
in our method, already declared in the
parameter list of the prototype.
Generally, the first step of our computation methods will be to receive values, and this is always done with parameters, not in the body of the method.
meters
=
feet
* 0.3048.Do this...
Using a declaration statement and an arithmetic expressions (as
you saw in the previous lab), write the code for this step. (Make
sure the return
statement stays at the end of the
method.)
meters
.The third statement can be performed using the Java
return
statement:
return-statement pattern |
return
|
Do this...
At the end of feetToMeters()
, write a better
return
statement that implements this last step of the
algorithm.
That's the end of the algorithm for the method.
Do this...
Now compile all of your code,
and run the test-case class for a
green bar. If you get errors compiling or running tests, fix the
problems until you see that green bar.
The tricky thing with writing tests is that initially errors
could be in your computation method (i.e.,
feetToMeters()
) or in the test method (i.e.,
testFeetToMeters()
). You'll have to check to make sure
that the tests are correct and that the computation is
correct. Once you get that green bar, though, the tests should be
fine, and further problems should be only in the computation
method.
Do this...
Add a comment before the method explaining what the method does,
mostly in terms of its parameters and return value.
You should also include any special cases that might surprise someone using the method (which we don't have this time around).
Here's one possibility:
/** * This method converts a number of feet into an equivalent number of * meters. * @param feet the number of feet. * @return the equivalent number of meters. */
The @param
and @return
are special Javadoc directives for specifying
parameters and return values. Each parameter should be specified
with its own @param
, followed by the name of the
parameter. There's only ever one value returned from a method, so
there's only ever one @return
.
Many people like to watch their method in action. The benefit of a test-case class is that it tests a lot of data automatically. But if you want interactive testing, then you want a driver.
A driver is a program that drives input, output, and computation. Usually, it refers to a simple program that drives just one or two methods to test them out interactively.
Most driver programs use the same algorithm:
Algorithm of any driver program
- Display a prompt for whatever values the method requires as arguments.
- Read those values from the keyboard.
- Do the computation that you're testing using the values from the keyboard.
- Display the result of the computation with appropriate labels.
Since our driver program uses this same basic algorithm, we can skip the entire design stage and proceed straight to coding.
Everything is implemented in a class, including drivers:
Do this...
First, create a driver class
named Driver
in the hotj.lab04
package.
You need access to the screen and keyboard:
Do this...
Add two import statements at the beginning of the file to
import the Screen
and Keyboard
classes of
the ann.easyio
package.
Now we can write code in the main()
method:
Do this...
Then add these statements to the beginning of the
main()
method:
Screen theScreen = new Screen(); Keyboard theKeyboard = new Keyboard();
Step 1 of the driver algorithm should be done after these
declarations. It's just a matter of using the
println()
method of the theScreen
object:
theScreen.println(expression
);
The expression
can be any expression at all that you want displayed on the screen.
For now, it should just be a String
literal like
"Enter the number of feet:"
.
Do this...
Add code for this first step of the driver.
Step 2 involves the keyboard and a declaration. We want to read
in a number of feet, a double
; and that should be
saved away for us in a variable. Every variable must be declared
first, so let's start there:
double feet = expression
;
Yes, we do declare feet
a second time
here, because this is a separate class and a separate method---a
different scope; each variable and parameter is
only valid within its own method.
The expression
here should be an expression that reads in a double
from the keyboard. Well, as good object-oriented programmers eager
to call methods, let's ask the keyboard itself to do the
reading:
theKeyboard.readDouble()
Do this...
Put this input expression into the declaration statement above to
complete Step 2 of the driver algorithm.
Next, you need to do the computation and save the result. This
will look a lot like the statement for feet
:
double meters = expression
;
The only difference here is that the value for
meters
does not come from the keyboard, but
from invoking Metric.feetToMeters()
.
Do this...
Type in the statement for this third step. See the method
invocations earlier in this lab exercise or in
MetricTest
for examples of this method invocation.
Finally, print the result with a useful message. We'll actually
use two methods from theScreen
object. First we need
the print()
we (almost) had before:
theScreen.print(expression
)
Just like println()
, the method
print()
will print the result of the expression
.
However, println()
finishes it off by also printing a
newline character; print()
does not.
As for the result of the conversion, notice that there's
no semi-colon at the end of that last statement. That's
because we're not quite finished with our statement.
Screen
objects, like theScreen
allow us
to chain along our method calls, so we'll add two more:
theScreen.print (expression1
) .printFormatted (realNumberExpression
,digits
) .println (expression2
);
expression1
and expression2
are going to be literal String
s to explain the
result.realNumberExpression
will be the number of meters.digits
is an
integer literal specifying how many digits after the decimal point
we'd like to see. This is an integer count of the digits,
not a real-number tolerance!Do this...
Use the full version of this statement, supplying the missing code
so that your output looks like this (assuming a certain input):
That many feet is equal to 0.000 meters.
Re-use the sample data from the test-case class when running the driver so that you know what answers you're supposed to get. Instead of testing the result automatically, though, the driver will print the result, and you'll have to check it.
Do this...
Add class documentation. You
should also add a comment for a
driver before main()
that explains what
main()
does.
Turn in all three code files, a sample execution of
MetricTest
and of Driver
(run
Driver
at least three times with different inputs each
time).
argument, call a method, computed value, driver, expected value,
fudge factor, input, invoke a method, library, method, method call,
method declaration, method definition, method stub, movement,
output, package, parameter, receive, return
, return,
return type, scope, signature, tolerance