Section 4.3
Parameters
IF A SUBROUTINE IS A BLACK BOX, then a
parameter provides a mechanism 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 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 3.2.
(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 to standard output, using
// startingValue as the initial value of N. It also
// prints the number of terms in the sequence.
// The value of the parameter, startingValue, must
// be a positive integer.
int N; // One of the terms in the sequence.
int count; // The number of terms.
N = startingValue; // The first term is whatever value
// is passed to the subroutine as
// a parameter.
int count = 1; // We have one term, the starting value, so far.
TextIO.putln("The 3N+1 sequence starting from " + N);
TextIO.putln();
TextIO.putln(N); // print initial term of sequence
while (N > 1) {
if (N % 2 == 1) // is N odd?
N = 3 * N + 1;
else
N = N / 2;
count++; // count this term
TextIO.putln(N); // print this term
}
TextIO.putln();
TextIO.putln("There were " + count + " terms in the sequence.");
} // end of Print3NSequence()
The parameter list of this subroutine, "(int startingValue)",
specifies that the subroutine has one parameter, of type int. When
the subroutine is called, a value must be provided for this parameter. This value
is assigned to the parameter, startingValue, before the body of the
subroutine is executed. For example, the subroutine could be called using the
subroutine call statement "Print3NSequence(17);". When the
computer executes this statement, the computer assigns the value 17 to
startingValue and then executes the statements in the subroutine.
This prints the 3N+1 sequence starting from 17. If K is
a variable of type int, then when the computer executes
the subroutine call statement "Print3NSequence(K);",
it will take the value of the variable K, assign that value to
startingValue, and execute the body of the subroutine.
The class that contains Print3NSequence can contain a
main() routine (or other subroutines) that call Print3NSequence.
For example, here is a main() program that prints out 3N+1 sequences
for various starting values specified by the user:
public static void main(String[] args) {
TextIO.putln("This program will print out 3N+1 sequences");
TextIO.putln("for starting values that you specify.");
TextIO.putln();
int K; // Input from user; loop ends when K < 0.
do {
TextIO.putln("Enter a starting value;")
TextIO.put("To end the program, enter 0: ");
K = TextIO.getInt(); // get starting value from user
if (K > 0) // print sequence, but only if K is > 0
Print3NSequence(K);
} while (K > 0); // continue only if K > 0
} // end main()
Note that the term "parameter" is used to refer to two different,
but related, concepts. There are parameters that are used in the
definitions of subroutines, such as startingValue in the above
example. And there are parameters that are used in subroutine
call statements, such as the K in the statement "Print3NSequence(K);".
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. When a subroutine
is called, the actual parameters in the subroutine call statement are
evaluated and the values are assigned to the formal parameters in the
subroutine's definition. Then the body of the subroutine is executed.
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. The type of the actual parameter must be one that could
legally be assigned to the formal parameter with an assignment statement. For example,
if the formal parameter is of type double, then it
would be legal to pass an int as the actual parameter
since ints can legally be assigned to doubles.
When you call a subroutine, you must provide one actual
parameter for each formal parameter in the subroutine's definition.
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; // Allocate memory locations for the formal parameters.
double x;
boolean test;
N = 17; // Assign 17 to the first formal parameter, N.
x = Math.sqrt(z+1); // Compute Math.sqrt(z+1), and assign it to
// the second formal parameter, x.
test = (z >= 10); // Evaluate "z >= 10" and assign the resulting
// true/false value to the third formal
// parameter, 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.)
Beginning programming students often find parameters to be surprisingly
confusing. Calling a subroutine that already exists is not a problem -- the idea of
providing information to the subroutine in a parameter is clear enough.
Writing the subroutine definition is another matter. A common mistake is
to assign values to the formal parameters at the
beginning of the subroutine, or to ask the
user to input their values. This represents a fundamental misunderstanding.
When the statements in the subroutine are executed, the formal parameters
will already have values. The values come from the subroutine call statement.
Remember that a subroutine is not independent. It is called by some other
routine, and it is the calling routine's responsibility to provide appropriate
values for the parameters.
In order to call a subroutine legally, you
need to know its name, you need to know how many formal parameters it
has, and you need to know the type of each parameter. This
information is called the subroutine's signature.
The signature of the subroutine doTask can be expressed as
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 formal parameter names are, so the
names are not part of the interface.
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 TextIO 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 acceptable programming style 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 subroutine's return type. 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 getln() { ... }
double getln() { ... }
So it should be no surprise that in the TextIO class, the
methods for reading different types are not all named getln().
In a given class, there can only be one routine that has the name getln
and has no parameters. The input routines in TextIO
are distinguished by having different names, such as getlnInt() and
getlnDouble().
Let's do a few examples of writing small subroutines to perform
assigned tasks. Of course, this is only one side of programming
with subroutines. The task performed by a subroutine is always a subtask
in a larger program. The art of designing those programs -- of deciding
how to break them up into subtasks -- is the other side of programming
with subroutines. We'll return to the question of program design in
Section 6.
As a first example, let's write a subroutine to compute and print out all the
divisors of a given positive integer. The integer will be a parameter to
the subroutine.
Remember that the format of any subroutine is
modifiers return-type subroutine-name ( parameter-list ) {
statements
}
Writing a subroutine always means filling out this format. The assignment tells us that
there is one parameter, of type int, and it tells us what the statements in the
body of the subroutine should do. Since we are only working with static subroutines for
now, we'll need to use static as a modifier. We could add an access modifier
(public or private), but in the absence of any instructions, I'll leave it
out. Since we are not told to return a value, the return type is void.
Since no names are specified, we'll have to make up names for the formal parameter and for the
subroutine itself. I'll use N for the parameter and printDivisors for
the subroutine name. The subroutine will look like
static void printDivisors( int N ) {
statements
}
and all we have left to do is to write the statements that make up the body of the
routine. This is not difficult. Just remember that you have to write the body
assuming that N already has a value! The algorithm is: "For each possible
divisor D in the range from 1 to N, if D
evenly divides N, then print D." Written in Java, this becomes:
static void printDivisors( int N ) {
// Print all the divisors of N.
// We assume that N is a positive integer.
int D; // One of the possible divisors of N.
System.out.println("The divisors of " + N + " are:");
for ( D = 1; D <= N; D++ ) {
if ( N % D == 0 )
System.out.println(D);
}
}
I've added comments indicating the contract of the subroutine -- that is, what it does and what
assumptions it makes. The contract includes the assumption that N is a positive
integer. It is up to the caller of the subroutine to make sure that this assumption
is satisfied.
As a second short example, consider the assignment: Write a subroutine named
printRow. It should have a parameter ch of type char and
a parameter N of type int. The subroutine should print out a line
of text containing N copies of the character ch.
Here, we are told the name of the subroutine and the names of the two parameters,
so we don't have much choice about the first line of the subroutine definition. The task
in this case is pretty simple, so the body of the subroutine is easy to write.
The complete subroutine is given by
static void printRow( char ch, int N ) {
// Write one line of output containing N copies of the
// character ch. If N <= 0, an empty line is output.
int i; // Loop-control variable for counting off the copies.
for ( i = 1; i <= N; i++ ) {
System.out.print( ch );
}
System.out.println();
}
Note that in this case, the contract makes no assumption about N, but it
makes it clear what will happen in all cases, including the unexpected case that
N < 0.
Finally, let's do an example that shows how one subroutine can build on another.
Let's write a subroutine that takes a String as a parameter. For each
character in the string, it will print a line of output containing 25 copies of
that character. It should use the printRow() subroutine to produce the
output.
Again, we get to choose a name for the subroutine and a name for the parameter.
I'll call the subroutine printRowsFromString and the parameter str.
The algorithm is pretty clear: For each position i in the string str,
call printRow(str.charAt(i),25) to print one line of the output. So, we
get:
static void printRowsFromString( String str ) {
// For each character in str, write a line of output
// containing 25 copies of that character.
int i; // Loop-control variable for counting off the chars.
for ( i = 0; i < str.length(); i++ ) {
printRow( str.charAt(i), 25 );
}
}
We could use printRowsFromString in a main() routine such as
public static void main(String[] args) {
String inputLine; // Line of text input by user.
TextIO.put("Enter a line of text: ");
inputLine = TextIO.getln();
TextIO.putln();
printRowsFromString( inputLine );
}
Of course, the three routines, main(), printRowsFromString(), and
printRow(), would have to be collected together inside the same class.The program
is rather useless, but it does demonstrate the use of subroutines.
You'll find the program in the file RowsOfChars.java,
if you want to take a look. Here's an applet that simulates the program:
I'll finish this section on parameters by noting that we
now have three different sorts of variables that can be used
inside a subroutine: local variables defined in the subroutine,
formal parameter names, and static member variables that are defined outside
the subroutine but inside the same class as the subroutine.
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 to the subroutine, as opposed to the
"local" variables defined inside 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. You've seen how
this works in the last example in the previous section,
where the value of the global variable, gamesWon, is computed inside a subroutine
and is used in the main() routine.
It's not always bad to use global variables in subroutines,
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. So before
you use a global variable in a subroutine, you should consider whether
it's really necessary.
I don't advise you to take an absolute stand against using
global variables inside subroutines. 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 can be very reasonable to let 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.