Week 07

Top-down Design With Methods
Version 1


 


Methods Avoid Repetition

  Believe it or not, you now know enough to write just about every program that could be written, if you don't care about things like graphics and fancy input and such. But writing big programs in the way we have been writing small ones would be a pretty nasty proposition. The problem is that there is no way to take advantage of the commonality that often exists between different parts of a large program. If you had some big calculation to perform with different values in different parts of a program, you'd have to repeat the appropriate code in each location.

Consider writing a program to print out the lyrics of the song "Ninety-Nine Bottles of Beer on the Wall". It would look something like:

Click Here To Run This Program On Its Own Page
// Program: BeerBottles
// Author:  Joshua S. Hodas
// Date:    October 24, 1996
// Purpose: To demonstrate the need for functions
 
import HMC.HMCSupport;
 
class BeerBottles {
 
  public static void main(String args[]) {
 
    HMCSupport.out.println("Ninety-nine bottles of beer on the wall");
    HMCSupport.out.println("Ninety-nine bottles of beer");
    HMCSupport.out.println("Take one down");
    HMCSupport.out.println("Pass it around");
    HMCSupport.out.println("Ninety-eight bottles of beer on the wall");
    HMCSupport.out.println();
    
    HMCSupport.out.println("Ninety-eight bottles of beer on the wall");
    HMCSupport.out.println("Ninety-eight bottles of beer");
    HMCSupport.out.println("Take one down");
    HMCSupport.out.println("Pass it around");
    HMCSupport.out.println("Ninety-seven bottles of beer on the wall");
    HMCSupport.out.println();
 
    // ... alot more ...
 
  }
 
}

This would go on and on, for ten or more pages. But notice that there is a lot of commonality in the program. For example, the two lines:

HMCSupport.out.println("Take one down");
HMCSupport.out.println("Pass it around"); 
which print the chorus, occur in exactly the same way in each and every verse.

The solution provided by essentially every programming language is to allow you to break your program up into a lot of different subprograms. In most languages these are called functions or procedures. In object-oriented languages such as Java and C++ they are referred to as methods, though here we are using them more in the traditional manner of functions.

  We have already defined one method, main, in all of our programs. There is nothing particularly special about main, other than that it is the method that is called automatically when the application is run. From now on our programs will have many methods. The method main will continue to be the one called when the applciation is started, but it may in turn call other methods we write to do some of the work. Of course, we have already been calling other methods (such as HMCSupport.out.println and Math.random) from within main, the only thing new is that the methods we will be calling will be ones we write ourselves.

So, for example, in the last program, it would make sense to define a method which prints the chorus of the song. The method would consist of the two lines of code, wrapped with the appropriate header information:

public void chorus() {    // A method to print the chorus of the song 

    HMCSupport.out.println("Take one down");
    HMCSupport.out.println("Pass it around");

} 
In order to call the method, we just give write the method name like any other we have used before. Since this is a method with no parameters, we follow the method name with an empty set of parentheses, as in:
chorus(); 

Putting this into our previous program, we get the following version:

Click Here To Run This Program On Its Own Page
// Program: BeerBottles2
// Author:  Joshua S. Hodas
// Date:    October 24, 1996
// Purpose: To demonstrate the use of  functions
 
import HMC.HMCSupport;
 
class BeerBottles2 {
 
  public static void main(String args[]) {
 
    HMCSupport.out.println("Ninety-nine bottles of beer on the wall");
    HMCSupport.out.println("Ninety-nine bottles of beer");
    chorus();
    HMCSupport.out.println("Ninety-eight bottles of beer on the wall");
    HMCSupport.out.println();
    
    HMCSupport.out.println("Ninety-eight bottles of beer on the wall");
    HMCSupport.out.println("Ninety-eight bottles of beer");
    chorus();
    HMCSupport.out.println("Ninety-seven bottles of beer on the wall");
    HMCSupport.out.println();
 
    // ... alot more ...
 
  }
 
 
  public static void chorus() {
 
    HMCSupport.out.println("Take one down");
    HMCSupport.out.println("Pass it around");
 
  }
 
}

This change shortens the program by more than a page, and it also has the advantage of making it crystal-clear that each verse shares those exact same two lines.


Methods Can Receive Information Effecting Their Behavior

  Of course, it is rare that you will have exactly the same lines duplicated in several places in a program. It is more likely that you might have the same process going on in several places, but manipulating different values. For example, in this program as it stands, there is little additional exact duplication. However, each verse consists of almost the same code; only the number of bottles changes.

Data Comes from Named Parameter Variables Methods can pass information to the methods they call, so that the behavior of those methods depends on the values they receive. As we have said earlier, the values passed are called parameters We have already sent parameters to methods to effect their behavior: each time we call HMCSupport.out.println we send it the value we want it to print; when we call the methods in the Math package, we generally send them the values with which we want them to compute.

To write a method whose behavior we want to vary based on the values it is sent, instead of writing the definition of the method hard-wired to a particular value, a variable is used in place of that value. Thus the code is parameterized over that value. For example, in the song-printing program, we can take all the code that depends on one particular value of the number of bottles of beer and make it refer to a variable holding the number of bottles of beer (as a string of words), as in:

numBottles = "ninety-eight";
HMCSupport.out.println(numBottles + " bottles of beer on the wall");
HMCSupport.out.println(); 
HMCSupport.out.println(numBottles + " bottles of beer on the wall");
HMCSupport.out.println(numBottles + " bottles of beer");
chorus(); 
If we do this throughout the whole program, we then notice that there is a new block of code that is repeated verbatim over and over in the program. Its behavior varies because the value of the variable it depends on changes. Now, this block of code actually overlaps two verses, but that is no problem. We just have to construct the program around it properly.

When a method is declared the declaration has to say that the method expects to receive a value. It also says what the method will call the value it receives, and what kind of value it will be. This is called declaring the formal parameters of the method. This declaration appears in the method header, which takes the form:

public static void methodName(type_1 pName_1 , ... , type_n pName_n) 

Since the chorus method didn't need any information from the calling code, its list of parameters was empty.

  Notice that every formal parameter name is preceded by a type. Unlike the declarations of variables within a method, the parameters to a method cannot be grouped with a single type declaration. So, for instance, if we are defining the method foo which takes three integer parameters, we must write:
public void foo(int a, int b, int c)  
We cannot write:
public void foo(int a, b, c)  

Since the method that prints a verse (roughly) of the song needs to receive a String holding the number of bottles, and since we have decided to call that variable numBottles, the header of the method is:

public static void verse(String numBottles) 

Inside the method, the formal parameter can be used just like any variable declared in the method. You can examine or change its value at will. (But note, as we will see below, changing its value will not effect the value of anything outside the method.)

Putting the header together with the code for the body of the method we get:

public void verse(String numBottles) {   // Method to print a verse
 
   HMCSupport.out.println(numBottles + " bottles of beer on the wall");
   HMCSupport.out.println();"
   HMCSupport.out.println(numBottles + " bottles of beer on the wall");
   HMCSupport.out.println(numBottles + " bottles of beer");
   chorus();
 
} 

Calling A Method With Parameters

To call the method we must now provide it with a String value to use as the number of bottles to be printed within the method. This value can come from any source that evaluates to a String. It could be a literal, as in:

verse("ninety-eight"); 
It could be a variable, as in:
String num = "ninety-eight";
verse(num); 
Or, it could be an arbitrary computation resulting in a String value:
String num = "ninety";
verse(num + "-eight"); 

When you call a method, the value, or variable, or expression you send to the method is called the actual parameter since it is the value that will actually be used for the formal parameter when the body of the method is executed.

Putting these ideas together, we can build a much shorter program to print the beer bottle song. Notice that because our verse method begins with the second half of the verse, the program begins with code to print the first part of the first verse, and ends with code to print the last line of the song (has anyone actually ever sung the last line of this song????):

Click Here To Run This Program On Its Own Page
// Program: BeerBottles3
// Author:  Joshua S. Hodas
// Date:    October 24, 1996
// Purpose: To demonstrate the use of parameterized functions
 
import HMC.HMCSupport;
 
class BeerBottles3 {
 
  public static void main(String args[]) {
 
    HMCSupport.out.println("Ninety-nine bottles of beer on the wall");
    HMCSupport.out.println("Ninety-nine bottles of beer");
    chorus();
 
    verse("ninety-eight");
    verse("ninety-seven");
    verse("ninety-six");
    verse("ninety-five");
    verse("ninety-four");
     
    // ... ninety-two more ...
 
    verse("one");
 
    HMCSupport.out.println("No more bottles of beer on the wall!!!");
 
  }
 
 
  public static void chorus() {
 
    HMCSupport.out.println("Take one down");
    HMCSupport.out.println("Pass it around");
 
  }
 
 
  public static void verse(String numBottles) {
 
    HMCSupport.out.println(numBottles + " bottles of beer on the wall");
    HMCSupport.out.println();
    HMCSupport.out.println(numBottles + " bottles of beer on the wall");
    HMCSupport.out.println(numBottles + " bottles of beer");
    chorus();
 
  }
 
}

For now all our method headers will continue to start with public static void. The void part will change just below, but we will not see methods that are not public static for a few more weeks.


Actual Parameters To Methods Are Copied

 
  It is important to note that when you pass an actual parameter to a method, the value of the actual parameter is copied into the formal parameter, which is a new variable that exists only within the method. Therefore, if a method makes an assignment to the variable named as the formal parameter, it does not affect the value as it exists in the method that called this one. For example, consider this simple program:

Click Here To Run This Program On Its Own Page
// Program: PassByValue
// Author:  Joshua S. Hodas
// Date:    October 27, 1996
// Purpose: To demonstrate an aspect of parameter passing
 
import HMC.HMCSupport;
 
class PassByValue {
 
  public static void main(String args[]) {
 
    int x;
 
    x = 4;
    HMCSupport.out.println("The value of x before call: " + x);
    increment(x);
    HMCSupport.out.println("The value of x after  call: " + x);
 
  }
 
 
  public static void increment(int x) {
 
    HMCSupport.out.println("Value of x at method start: " + x);
    x = x + 1;
    HMCSupport.out.println("Value of x at method   end: " + x);
 
  }
 
}

The output of this program is:

The value of x before call: 4
Value of x at method start: 4
Value of x at method   end: 5
The value of x after  call: 4
Even though we increment the parameter inside the method, that does not effect the variable holding that value in the calling method. This is a very important aspect of the way that Java works. When sending parameters to a function it uses a protocol known as pass-by-value, which means that it is the value of the actual parameter that is sent, not the actual parameter itself.

When you think about it, this makes perfect sense. The method increment was declared as expecting an int as a parameter. As we know, it would have been perfectly legitimate to use the method with the following call:

increment(4); 
since we said that the actual parameter in a method call can be a literal, a variable, or any other expression that has the right type. But since the value being passed is not in a variable, if we did not first copy it into a new variable used in the method, how could we increment it. You can't change the value of a literal!


Don't Get Confused About Parameter Names

 

  At the risk of beating a dead horse (apologies to the animal rights activists in the audience!), it is extremely important to understand the following ideas:
  1. Every method has its own set of variables, consisting of the variables named as formal parameters in the method header and the variables declared inside the method.

    A method can only refer to its own variables.

  2. You should pick names for your variables and formal parameters that make sense in the context of the method in which they are being declared.

    For example, suppose you are writing a program which manipulates grades. In it you write a method max which takes two grades and determines which is the larger one and returns it. In that context, the fact that the two numbers it is being called with are grades is not important, and it would be bad style to name the formal parameters grade1 and grade2. Doing so would mean that you'd want to edit the code for the method if you later wanted to use it in a program that manipulates some other sort of values. Better to name the parameters something more generic, since that is how the max method is thinking of them. Then the code for the method can be lifted without change.

  3. The fact that two variables in different methods have the same name is unimportant. It is neither good nor bad style, and it does not mean that they contain the same value, refer to the same location in memory, or are connected in any other way. (It is good style if it happens coincedentally out of an effort to give variables names that are sensible in context. It is bad style if it happens in an attempt to set up some correspondence, even though none exists.)

    Even if the actual parameter passed to a method in the method call has the same name as the formal parameter in the method definition, this sets up no special relationship, and doesn't make it any more possible for the code in the method to change the value of the variable in the calling method.

  This may all seem a bit confusing, but really it is no different that what you have been doing in math for years. Certainly you would have no difficulty writing, or understanding, the following bit of math prose:
Let us define the function f(x) = x * x. Then, f(2) is 4. If y is 3, f(y) is 9. On the other hand, for x equal to 5, f(x) is 25.
When we give the definition of the function f, it is as a schematic computation, whose actual value is later filled in for a given value of the parameter variable x. When we refer to f(2) it is as the result of carrying out the schematic computation on the value 2. When we refer to f(y) it is the result of evaluating that schematic expression for the value 3, since that is the value of the variable y, which the function is being applied to. That is, the value of y is the value that is used for the value of x for as long as we are evaluating the expression that defines the function. Finally, when we refer to f(x) in the last sentence, the value of x, which is 5, is used as the value of x while evaluating the expression. The fact that the value came from a variable named x has nothing to do with the use of the name x in the definition. The x in the definition refers to whatever value the function is being applied to.


Methods Can Return Information As Their Result

  So far, all the methods we have defined simply execute some process (possibly taking parameters to vary their behavior) and then exit. In many cases (in fact, in most cases) the task of a method is to gather or compute some value and communicate it back to the method that called it. The method is said to return this value.

The Method Header Specifies The Type Of Value Returned If a value is to be returned, the header of a method must declare the type of value that the method will return. This type is given after the public static part of the header and before the method's name. Putting the type here enables the compiler to insure that the calculations inside the method are correct in that they are at least producing the type of value you intended. It also allows the compiler to check that calls to the method are written in such a way that they are prepared for a value of this type. So for example, if the method foo were defined to take an int as a parameter and to return a double as a result, as in:
public static double foo(int n) 
then the compiler would know that the call:
int y = foo(3); 
should generate an error (since we are trying to take a double value and put it in the int variable y).

Since all of the methods we have defined so far have not returned values, we have been using the type void in this position, which indicates that no value is returned. Any valid Java type (or class) can go in this position. For example, the following code defines a method which takes a double value as an argument and computes and returns the square of whatever value it is given.

public static double square(double x) {
 
    return x*x;
   
} 
In this example, the return type is the same as the parameter type, but that is certainly not always true. For example, we can define the following method which takes a long as a parameter and responds whether that parameter is even or not:
public static boolean even(long x) {
 
   if (x % 2 == 0) {
 
     return true;
   }
   else {
 
     return false;
   }
} 
Recall that, even though we have declared the formal parameter to be a long, you could pass in an int value as the actual parameter in a call to the function without using an explicit cast. Since the copying of the actual parameter's value to the formal parameter's storage is essentially the same as an assignment operation, the automatic casting rules say that an int can be cast up to a long without damage.

Of course this method is more complicated than it needs to be. Think about what the overall structure of the if statement means: "if this expression evaluates to true, then return true, otherwise (that is, if it evaluates to false), return false." But that's silly: we are just returning whatever the test expression evaluates to. There is really no reason for the if statement at all. So, the method can be shortened to:

public static boolean even(long x) {
 
   return (x % 2 == 0) 
 
} 

return Is A Flow Control Statement Notice that in the first version of the last example there were two uses of the return statement. Clearly, though, only one of the two would get executed since they are in opposite branches of a conditional statement. It is important to understand, though, that only one return statement is ever executed in a given call to a method. At the moment the statement is executed, the value specified is returned, and flow returns to the point where the method was called. Thus, though the original is probably better style, you could also write this method as:
public static boolean even(long x) {
 
  if (x % 2 == 0) {
 
    return true;
  }
 
  return false;
   
} 
In this version, if the parameter passed is even, the test succeeds and the first return statement is executed. The execution of the method stops immediately, and the value true is returned to the point where the method was called. If the parameter is odd, then the test fails, and the body of the if (containing the first return statement) is skipped over. At that point the next statement encountered is the second return statement. It is executed and the method returns false.


Common Errors with return Statements

 

Placing Code After A return If there were any additional code after the second return statement in the last example, it would never get executed in any circumstance. The same would be true of any code inside the if block but after the first return statement. Therefore, the compiler will alert you to such useless code by generating a message like:
foo.java:27: Statement not reached.
        x = 3;
        ^ 
pointing to a line that can never be executed.

Forgetting To return A Value A common error is to define a method to compute some value, but to forget to actually return the value at the end. For example, we might write:
public static double square(double x) {
 
    double square;
 
    square = x * x;
} 
Surely, the compiler realizes that since we went to all the trouble to compute the value of the square, and even named the method indicatively, that we intend to return that result! But of course the compiler cannot make that assumption. There might be several values computed in the course of a method. The compiler cannot presume to guess which one you consider to be the final result. So, in this case, it will complain with an error like:
foo.java:21: Return required at end of double square(double). 

Note that if the return statement occurs in one branch of a conditional, it must also occur in the other branch (or at some point later in the method that the compiler can be sure will be executed). Otherwise the compiler will not be convinced that the flow will always lead to a use of return.

Misusing return In A void Method If you define a function as void and attempt to return a value, the compiler will complain, since no value should be returned. Similarly, if you call a void method in a way that expects a return value (for example on the right hand side of an assignment, or inside a larger arithmetic expression) the compiler will also reject it.

Proper Use Of return In A void Method Nevertheless, the return statement can be used within void methods. In such cases it is simply used without a value to return, and is instead followed immediately by the semicolon, as in:
return; 
In these uses its purpose is simply to force an immediate exit from the method, instead of the ordinary exit which occurs when the flow reaches the last statement in the method.


Top-Down Design With Methods

  Let's look at a slightly larger example of computing with methods. Consider the problem of computing the number of ways of selecting a group of k objects from a population of n distinct objects. This value (called n choose k) is given by the formula:

One possible top-down design for a program that computes these values for the user might be:

Program to Compute Number of Combinations
Tools:
Input/Output Routines

Ingredients:
Integer Variables: n, k.

Steps:

  1. Prompt for and get the values of n and k.
  2. Calculate n choose k.
    1. Calculate n factorial
    2. Calculate k factorial
    3. Calculate n-k factorial
    4. Calculate the result as given in the equation.
  3. Print the result.

Methods Capture Repeated Tasks At this point in the top-down design it becomes obvious that it would be better to write a function for computing factorials than to repeat the necessary for-loop three separate times in our code. This will have two effects. Obviously, it will save some typing and paper. However it will also make the program clearer and more natural. Rather than having to look at a few-line-long loop and figure out that it computes the factorial (assuming we don't include a comment at the beginning of each loop saying what it does), the reader will instead see method invocations that look something like:
nfact = factorial(n); 
What could be easier to understand?

Methods Capture Units of Design Even when a block of code occurs only once in a program, it is often a good candidate for a method. By gathering the code together and assigning it a name, the program using that code becomes clearer and more well-structured. In addition, you might later find yourself writing a program in which you need that same task several times. By starting to write your programs as collections of methods, you slowly build up a library of methods that you can choose pieces from at a later date. These selected methods can be dropped into new programs with little or no modification, since they communicate with the overall program only through parameter passing and return values, and don't depend on how variables were named in the overall program.

For example, even though this program will only compute n choose k once, it seems a natural choice for a method. It will make this program's main method easier to read, and if someday later I need to compute combinations for a larger probability/statistics application all I need to do is grab this method (and the ones it relies on) and include them in that program.

In the end, it is not uncommon to have a method for nearly each line of each level of the top-down-design for a program. Each block of code then reads like a higher-level outline of a process implemented by the methods at the next lower level.

Putting these ideas together, we come up with the following program:

Click Here To Run This Program On Its Own Page
// Program: NChooseK
// Author:  Joshua S. Hodas
// Date:    October 27, 1996
// Purpose: To demonstrate Top-Down-Design with Methods
 
import HMC.HMCSupport;
 
class NChooseK {
 
  public static void main(String args[]) {
 
    int pop, group;
 
    HMCSupport.out.println("This program computes the "
                           + "number of combinations "
                           + "possible from a population.");
 
    HMCSupport.out.print("Enter the population size: ");
    pop = HMCSupport.in.nextInt();
    HMCSupport.out.print("Enter the selected group size: ");
    group = HMCSupport.in.nextInt();
 
    HMCSupport.out.println("There are " + choose(pop,group) +
                           " ways of selecting " + group +
                           " objects from " + pop + " objects.");
  }
 
 
  public static long choose(int n, int k) {
 
    return (factorial(n) / (factorial(n-k) * factorial(k)));
 
  }
 
 
  public static long factorial(int n) {
 
    long product = 1;
 
    for (int i = 2 ; i <= n ; i++) {
 
      product *= i;
 
    }
 
    return product;
 
  }
 
}


Organizing Your Methods

  In some languages, such as C, it is necessary to define a method before any other method that uses is. This makes the job of the compiler easier in that it will always have seen the method definition and its associated types by the time it has to decide whether some use of the method is legitimate. Some languages relax this requirement by allowing you to put what is called a prototype of the method (basically just a copy of the method header) early in the program but leave the actual definition of the method till later.

Java does not have any requirements on the order of methods in a class definition. The compiler just makes multiple passes through the code to make sure all the types match up properly. This means that you can organize the methods in any way you prefer. There is no fixed style rule for this, but one good choice is to organize the methods in order by their level in the top-down design. The method ("main")} comes first, providing a very high-level view of the program's structure. This is followed by the methods the main calls, which together give a view of the next level of the detail. Then come the methods that these methods call, and so on.

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, Oct 4, 1998 at 12:15:32 PM.
http://www.cs.hmc.edu/~hodas/courses/cs5/week_07/lecture/lecture.html