Section 3.3

IF A SUBROUTINE IS A BLACK BOX, then a parameter provides a method for passing information from the outside world into the box. Parameters are part of the interface of a subroutine. They allow you to customize the behavior of a subroutine to adapt it to a particular situation.

As an analogy, consider a thermostat -- a black box whose task it is to keep your house at or near a certain temperature. The thermostat has a parameter, namely the dial that is used to set the desired temperature. The thermostat always performs the same task: maintaining a constant temperature. However, the exact task that it performs -- that is, which temperature it maintains -- is customized by the setting on its dial.

As an example, let's go back to the "3N+1" problem that was discussed in Section 2.5. (Recall that a 3N+1 sequence is computed according to the rule, "if N is odd, multiply by 3 and add 1; if N is even, divide by 2; continue until N is equal to 1." For example, starting from N=3 we get the sequence: 3, 10, 5, 16, 8, 4, 2, 1.) Suppose that we want to write a subroutine to print out such sequences. The subroutine will always perform the same task: Print out a 3N+1 sequence. But the exact sequence it prints out depends on the starting value of N. So, the starting value of N would be a parameter to the subroutine. The subroutine could be written like this:

         static void Print3NSequence(int startingValue) {
            // prints a 3N+1 sequence on the console, using
            // startingValue as the initial value of N

            int N = startingValue;  // N represents a term in the sequence
            int count = 1;   // count is the number of terms found
            console.putln("The 3N+1 sequence starting from " + N);
            console.putln(N);  // print initial term of sequence
            while (N > 1) {
                if (N % 2 == 1)     // is N odd?
                   N = 3 * N + 1;
                   N = N / 2;
                count++;   // count this term
                console.putln(N);  // print this term
            console.putln("There were " + count + " terms in the sequence.");
         }  // end of Print3NSequence()

This subroutine could be called using statements such as "Print3NSequence(17);", which would print out the 3N+1 sequence starting from 17. If K is a variable of type int, then the statement "Print3NSequence(K);" would print out the 3N+1 sequence starting from K, that is from what ever happens to be the value of K when the statement is executed.

We might use this subroutine in a program that lets the user print out sequences for a number of different starting values:

         public static void main(String[] args) {
            console = new Console();
            console.putln("This program will print out 3N+1 sequences");
            console.putln("for starting values that you specify.");
            do {
               console.putln("Enter a starting value;")
               console.put("To end the program, enter 0: ");
               int K = console.getInt();  // get starting value from user
               if (K > 0)   // print sequence, but only if K is > 0
            } while (K > 0);   // continue only if K > 0
         } // end main()

There are actually two very different sorts of parameters: the parameters that are used in the definition of a subroutine, and the parameters that are passed to the subroutine when it is called. Parameters in a subroutine definition are called formal parameters or dummy parameters. The parameters that are passed to a subroutine when it is called are called actual parameters.

A formal parameter must be an identifier, that is, a name. A formal parameter is very much like a variable, and -- like a variable -- it has a specified type such as int, boolean, or String. An actual parameter is a value, and so it can be specified by any expression, provided that the expression computes a value of the correct type. When you call a subroutine, you must provide one actual parameter for each formal parameter in the subroutine's definition. The computer evaluates each actual parameter and initializes the corresponding formal parameter with that value. Consider, for example, a subroutine

         static void doTask(int N, double x, boolean test) {
             // statements to perform the task go here

This subroutine might be called with the statement

doTask(17, Math.sqrt(z+1), z >= 10);

When the computer executes this statement, it has essentially the same effect as the block of statements:

        int N = 17; // declare an int named N with initial value 17
        double x = Math.sqrt(z+1);  // compute Math.sqrt(z+1), and 
              // use it to initialize a new variable x of type double
        boolean test = (z >= 10); // evaluate "z >= 10"
              // and use the resulting true/false value to initialize
              // a new variable named test
        // statements to perform the task go here

(There are a few technical differences between this and "doTask(17,Math.sqrt(z+1),z>=10);" -- besides the amount of typing -- because of questions about scope of variables and what happens when several variables or parameters have the same name.)

You can see that in order to call a subroutine legally, you need to know its name, you need to know how many parameters it has, and you need to know the type of each parameter. This information is called the subroutine's signature. We could write the signature of the subroutine doTask as: doTask(int,double,boolean). Note that the signature does not include the names of the parameters; in fact, if you just want to use the subroutine, you don't even need to know what the names are, so the names are not part of the interface. The names are only used by the person who writes the subroutine.

Java is somewhat unusual in that it allows two different subroutines in the same class to have the same name, provided that their signatures are different. (The language C++ on which Java is based also has this feature.) We say that the name of the subroutine is overloaded because it has several different meanings. The computer doesn't get the subroutines mixed up. It can tell which one you want to call by the number and types of the actual parameters that you provide in the subroutine call statement. You have already seen overloading used in the Console class. This class includes many different methods named putln, for example. These methods all have different signatures, such as:

      putln(int)       putln(int,int)      putln(double)
      putln(String)    putln(String,int)   putln(char)
      putln(boolean)   putln(boolean,int)  putln()

Of course all these different subroutines are semantically related, which is why it is OK to use the same name for them all. But as far as the computer is concerned, printing out an int is very different from printing out a String, which is different from printing out a boolean, and so forth -- so that each of these operations requires a different method.

Note, by the way, that the signature does not include the return type of the subroutine. It is illegal to have two subroutines in the same class that have the same signature but that have different return types. For example, it would be a syntax error for a class to contain two methods defined as:

        int    get() { ... }
        double get() { ... }

So it should be no surprise that in the Console class, the methods for reading different types have different names such as getInt() and getDouble().

I'll finish this section on parameters by noting that we now have three different sorts of variables that can be used inside subroutines: local variables defined in the subroutines, formal parameter names, and variables that are defined outside the subroutine that are members of the same class.

Local variables have no connection to the outside world; they are purely part of the internal working of the subroutine. Parameters are used to "drop" values into the subroutine when it is called, but once the subroutine starts executing, parameters act much like local variables. Changes made inside a subroutine to a formal parameter have no effect on the rest of the program (at least if the type of the parameter is one of the primitive types -- things are more complicated in the case of objects, as we'll see later).

Things are different when a subroutine uses a variable that is defined outside the subroutine. That variable exists independently of the subroutine, and it is accessible to other parts of the program, as well as to the subroutine. Such a variable is said to be global, as opposed to the "local" variables of the subroutine. The scope of a global variable includes the entire class in which it is defined. Changes made to a global variable can have effects that extend outside the subroutine where the changes are made. This is not necessarily bad, but you should realize that the global variable then has to be considered part of the subroutine's interface. The subroutine uses the global variable to communicate with the rest of the program. This is a kind of sneaky, back-door communication that is less visible than communication done through parameters, and it risks violating the rule that the interface of a black box should be straightforward and easy to understand.

I don't advise you to take an absolute stand against using global variables inside procedures. There is at least one good reason to do it: If you think of the class as a whole as being a kind of black box, it might be reasonable to have the subroutines inside that box be a little sneaky about communicating with each other, if that will make the class as a whole look simpler from the outside.

However, you should definitely avoid using a global variable when a parameter would be more appropriate. (This is to some extent a matter of judgement and taste, of course.)

[ Next Section | Previous Section | Chapter Index | Main Index ]