Lab 4: Methods


 

Introduction

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 the idea of 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 a generalization 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.

The ideas of sharing, libraries and reuse are very important in Java, and we have already (unwittingly) used them extensively. For example, in the previous lab exercises, we wrote

   import ann.easyio.*

Recall that the effect of this directive is to tell the compiler that we want to use the classes contained in the directory ann.easyio. Recall also that each of the programs we have written has reused this same set of classes. The directory ann.easyio is thus similar to a library of books, in that it contains a set of classes that can be shared and reused by any program that needs them.

Software libraries are particularly useful and important, because with carefully planning, they can reduce the cost of programming. For example, in a previous lab exercise, we used the sqrt() method from the Java Math class, and so avoided the extra "cost" of having to write our own square root method. Libraries thus provide a place where groups of related methods can be stored, so that we (or even other programmers) can access them and thus avoid "reinventing the wheel."

There are two kinds of ways that we can bundle things in Java. Java allows us to group related methods together into a single class.

Java also allows us to group related classes together into a package (also know as a class library.) While Java provides a number of ready-made packages for us, Java also permits programmers to create their own packages.

We will not create a complete package today, but will just create a class containing some useful methods (a la the Math class).

 

Review

Let us briefly review what we know about libraries. 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 java.lang.Math

for example, would let your program use the Math class. We don't need to use this import only because Java automatically imports all of the classes in the java.lang package.

To call a method, you must give its name and a pair of parentheses containing any arguments the method needs to do its job. For example:

   result = Math.pow(x, y);

When control reaches such a method call, the arguments x and y are evaluated and passed to the method, which performs its task and (if appropriate) returns a value back to its caller (in this case x^y).

 

Planning a Module

To keep our presentation simple, we will construct our class in our folder for this exercise so begin by creating a folder (e.g., lab4) in which to store your work. Copy the files Metric.java and Driver.java to your project folder. As the name Metric suggests, the class we will create today will provide us with a set of methods to convert English-system measurements into their metric-system counterparts.

If you are using an integrated developement environment (IDE) like CodeWarrior, create a new project named Metric in your project folder, and then add Metric.java, Driver.java, and the ann and hoj packages into your project.

The first thing that we must decide is what measurement conversions we wish our class to provide. 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

Our exercise today is to write a subprogram (more precisely, a method) to convert feet into meters. By storing this method in class Metric, it can be shared by any and all programs that need to convert feet into meters, allowing them to avoid "redefining the wheel."

 

Structure

A class is stored in an implementation file in which you define each name accessible outside the class.

In the language C++, you would need a third file where you would declare a prototype for each of the methods. The prototype consists of

Why this extra separation? The reason has to do with program maintenance. If we are writing a class, then we expect that a number of programs will make use of it. It is often the case that even a well-designed class may need to be updated (i.e., maintained), if a better way is discovered to perform one of its methods. If we have designed our methods carefully, then updating a method should simply involve altering its definition, but not its prototype.

Now suppose that a program could access and make use of the definition details of a class. If it did, then updating the method will likely change those details, and if the program were written in such a way as to be dependent on those details, then it would have to be updated, too. That is what we want to avoid at all costs -- we should never have to modify a program as a result of updating a method a class provides.

Method Design

As we have seen, software that is carefully designed is better than software that is poorly designed. The same holds true for methods, so let's spend a few moments using object-centered design to design our method.

 

Behavior

   Our method should receive from its caller the number of feet
   to be converted, and should check that this value is positive.
   It should convert that quantity to meters by multiplying it by
   0.3048, and return the resulting value to the caller.

Note that where a program typically inputs values from the keyboard, a method typically receives values from whoever called the method. Similarly, where a program typically outputs values to the screen, a method typically returns a value to whoever called the method.

Note also that we want to try and anticipate what could go wrong. Since the method will only produce a correct result if the value it receives is not negative, we say that this method has a precondition -- a condition that must be true in order for the method to execute correctly.

 

Objects

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?

Description

Type

Kind

Movement

Name

the number of feet

double

varying

in

feet

the conversion factor

double

constant

local

0.3048

the corresponding number of meters

double

varying

out

meters

This information provides us with what we need to specify the problem our method solves:

   Specification:
      receive: feet, the (double) number of feet to be converted.
      precondition: feet is not negative.
      return: meters, the (double) equivalent of feet in meters.

Note that we include the method's precondition as part of its specification.

This specification in turn provides us with what we need to write a prototype. The simplified pattern for a prototype is

   SpecialMods ReturnType MethodName ( ParameterDecList )

where

That is, a parameter must be declared for each value that a method receives from its caller.

A precise method specification tell us how to build the prototype. To illustrate:

  1. Since feetToMeters() is to be a part of the class Metric, its SpecialMods include public. The method will be called via Metric.feetToMeters(). This means that the method is being called with the class and not an instance (object) of type Metric. Therefore, the method will be static as well.
  2. The specification tells us that feetToMeters() must return a real value to its caller, and so its ReturnType should be double.
  3. The specification also tells us that feetToMeters() must receive one real value from its caller (the number of feet to be converted), so feetToMeters() should have one double parameter, which we will call feet.

The prototype of our method is:

   public static double feetToMeters(double feet)

Since parameters are like variables, their names usually begin with a lowercase letter, with every word after the first capitalized.

As this illustrates, the stages of software design are fluid, not firm. We can build a prototype (a part of coding) during our design stage, as soon as we have enough information. (And the specification provides all of the necessary information to write the prototype.)

 

Operations

Continuing with our design, our list of needed operations is quite short:

Description

Predefined?

Name

Module?

get a value from the caller

yes

parameter mechanism

built-in

check that the value is not negative

yes

value >= 0

built-in

halt the program if the value is not negative

yes

check()

hoj.Assertion

multiple two real values

yes

*

built-in

return a value to the caller

yes

return

built-in

As we shall see, the Java parameter mechanism permits a method to automatically receive a value from its caller, and the Java return statement permits a method to return a value to its caller.

To test our precondition and halt the program if it fails, we can use the Assertion.check() mechanism and a boolean expression that returns true if and only if feet is not negative.

 

Algorithm

We can then organize these operations and objects into the following algorithm:

   1. Receive feet from the caller.
   2. Halt the program if feet is negative.
   3. Compute meters = feet * 0.3048.
   4. Return meters.

Given an algorithm, we are ready to define feetToMeters() in our implementation file.

 

Implementation File Structure

As stated previously, the definitions of a class and its methods are stored in an implementation file (whose name is the same as the name of the class), so open the file Metric.java. If you are using an IDE, make sure that the implementation file is a part of your project.

The general pattern for an implementation file is as follows:

   OpeningDocumentation

   public class ClassName 
   {

   DefinitionList

   }

Take a moment to personalize the opening documentation.

 

Defining A Method (Coding)

The general pattern for a method definition is

   SpecialMods ReturnType MethodName ( ParameterDefList )
   {
      StatementList
   }

where

A method stub is a minimal method definition -- like a prototype, but followed by a pair of empty braces. For example, we might define the following stub for feetToMeters():

   public static double feetToMeters (double feet)
   {
   }

Insert this stub at the end of the file Metric.java, then compile just that file.

If all is well, the method stub in Metric.java should compile and generate an error message like:

  Error   : Return required at end of double feetToMeters(double).
  Metric.java line 14    static double feetToMeters (double feet)
 

This is appropriate, since our stub does not return anything. You may also get a warning that feet is not used. If you get errors besides these, find and correct them before continuing.

Take a moment and consider what we just did. We just compiled a file that contains no main program and the only errors the compiler generated were that our method stub didn't use its parameter, and it had no return value. How can this be?

The answer has to do with the idea of separate compilation. Recall that translation of a program is actually a two-step process:

  1. compiling the source file creates a Java byte code object file (in an IDE, this file is stored in the project), and
  2. linking/loading the object files to be run by the interpreter.

It is the linker/loader that requires a program to have a main function, not the compiler.

Once we have a stub, we can "fill it in" using our algorithm and stepwise translation.

 

1. Receive feet from the caller.

This step is taken care of for us by the Java method-call mechanism. When the caller evaluates an expression that calls our method, such as:

   Metric.feetToMeters(2.5);

a copy of the argument 2.5 will automatically be stored in parameter feet in our method. That is, when our method begins execution, the value of feet will be the value of the argument with which the method was called (2.5, in this example).

 

2. Halt the program if feet is negative.

We can accomplish this step using the Assertion class in the hoj package. It has a method check() which can be invoked as:

   Assertion.check(BooleanExpression);

Given a boolean expression, it allows execution to proceed normally if the expression evaluates to true, but halts the program and displays a diagnostic message if it evaluates to false. We can thus test our method's precondition by adding the line

   Assertion.check(feet >= 0);

as the first statement in the method. To use check(), you must import the class hoj.Assertion.

 

3. Compute meters = feet * 0.3048.

Using the declaration statements and expressions that we learned about in lab 2, add a declaration statement to feetToMeters() that performs step 3 of our algorithm. Recompile Metric.java, and continue when you get no new errors.

 

4. Return meters.

The third operation can be performed using the Java return statement, whose general pattern is:

   return Expression;

where Expression is any valid Java expression.

In the stub of feetToMeters(), write a return statement that accomplishes the third operation. Then recompile Metric.java. This time, you should get no compilation errors for Metric.java. When Metric.java is free of errors, continue on in the exercise.

To see the difference compilation and linking/loading, use make (or build in some IDEs) to translate your project and then run it. You should get a run time error message like:

   The main class Driver
   does not have a static public void main(String args[]) method
 

which essentially means the loader was unable to find a main function. Since testing feetToMeters() requires that we call it from a program, let's proceed to writing a program to test our method.

 

Writing a Program to Test the Module

Next, open Driver.java. If you are using an IDE, make sure it has been added to your project. In this file we will create a program whose sole purpose is to test feetToMeters(). Such programs are called driver programs, because all they do is "test-drive" the methods in a class.

 

Designing The Driver Program

Most driver programs use the same algorithm:

   1. Display a prompt for whatever values the method requires as arguments.
   2. Read those values from the keyboard.
   3. Display the result of calling the method,
       using the input values as arguments (plus a descriptive label).

Since our driver program uses this same basic algorithm, we can skip the entire Design stage and proceed straight to Coding.

 

Coding the Driver Program

Personalize the opening documentation in Driver.java. Then add a Java program containing the necessary output, declaration, and input statements to perform steps 1 and 2 of our algorithm. Don't forget to add an import directive for the package ann.easyio .

To perform step 3, recall that an argument is a value that is passed to a method when the method is called. For example, if we were to write:

   theScreen.println("\nThe number of meters is "
           + Metric.feetToMeters(inValue) );

then the value of inValue would be the argument in this call to feetToMeters().

There are several important rules to remember when calling a method:

  1. The number of arguments in a method call must equal the number of parameters in the method's prototype, or a compilation error will occur.
  2. The types of the first argument and first parameter, the second argument and second parameter, the third argument and third parameter, etc. must be the same, or a compilation error will occur.
  3. A static method like feetToMeters() defines a message we must send to its class (i.e., Metric), so we write an expression like this to invoke it:
           Metric.feetToMeters(inValue)
       

 

Multi-File Translation

Since our program involves more than one file (i.e., Driver.java and Metric.java), translating it is slightly more complicated from the compiler's perspective.

The make command (or build in some IDEs) coordinates the multifile translation. Use make to translate your project and fix any syntax errors that you find.

 

Testing

Once our program is successfully translated, we are ready to test what we have written, to see if we can discover any logic errors. Execute the program. Use the following sample data to test the correctness of feetToMeters():

Feet

Meters (Predicted)

Meters (Observed)

1.0

0.3048

______________

3.3

1.00584

______________

Since feetToMeters() is a linear function of it's parameter, two test values are sufficient to verify its correctness. Be sure to note and correct any discrepancies. When Driver executes feetToMeters() correctly, our task is finished!

 

Run your program and print a hard copy of its output. Save your work and then clean out your project as directed by your instructor.

 

Phrases you should now understand:

Module, Method, Parameter, Argument, Driver Program.


 

Submit:

Hard copies of your final versions of Driver.java, and Metric.java, plus a hard copy showing the execution of your program.


Back to This Lab's Table of Contents

Back to the Prelab Questions

Forward to the Homework Projects


Back to the Table of Contents

Back to the Introduction


Copyright 2000 by Prentice Hall. All rights reserved.