In a Java program, all parameters are passed by value. However, there are three other parameter-passing modes that have been used in programming languages:
First, here's some useful terminology:
void f(int a, boolean b, int c)we will use the terms parameters, formal parameters, or just formals to refer to a, b, and c.
f(x, x==y, 6);we will use the terms arguments, actual parameters, or just actuals to refer to x, x==y, and 6.
expression | r-value | |
x | 2 | |
y | 3 | |
x+y | 5 | |
x==y | false |
Parameters can only be passed by value in Java and in C. In Pascal, a parameter is passed by value unless the corresponding formal has the keyword var; similarly, in C++, a parameter is passed by value unless the corresponding formal has the symbol & in front of its name. For example, in the Pascal and C++ code below, parameter x is passed by value, but not parameter y:
// Pascal procedure header Procedure f(x: integer; var y: integer); // C++ function header void f(int x; int & y);When a parameter is passed by value, the calling method copies the r-value of the argument into the called method's AR. Since the called method only has access to the copy, changing a formal parameter (in the called method) has no effect on the corresponding argument. Of course, if the argument is a pointer, then changing the "thing pointed to" does have an effect that can be "seen" in the calling procedure. For example, in Java, arrays are really pointers, so if an array is passed as an argument to a method, the called method can change the contents of the array, but not the array variable itself, as illustrated below:
void f( int[] A ) { A[0] = 10; // change an element of parameter A A = null; // change A itself (but not the corresponding actual) } void g() { int[] B = new int [3]; B[0] = 5; f(B); //*** B is not null here, because B was passed by value //*** however, B[0] is now 10, because method f changed the first element //*** of the array pointed to by B }
What is printed when the following Java program executes and why?
class Person { int age; String name; } class Test { static void changePerson(Person P) { P.age = 10; P = new Person(); P.name = "Joe"; } public static void main(String[] args) { Person P = new Person(); P.age = 2; P.name = "Ann"; changePerson(P); System.out.println(P.age); System.out.println(P.name); } }
When a parameter is passed by reference, the calling method copies the l-value of the argument into the called method's AR (i.e., it copies a pointer to the argument instead of copying the argument's value). Each time the formal is used, the pointer is followed. If the formal is used as an r-value (e.g., its value is printed, or assigned to another variable), the value is fetched from the location pointed to by the pointer. If the formal is assigned a new value, that new value is written into the location pointed to by the pointer (the new value is not written into the called method's AR).
If an argument passed by reference has no l-value (e.g., it is an expression like x+y), the compiler may consider this an error (that is what happens in Pascal, and is also done by some C++ compilers), or it may give a warning, then generate code to evaluate the expression, to store the result in some temporary location, and to copy the address of that location into the called method's AR (this is is done by some C++ compilers).
In terms of language design, it seems like a good idea to consider this kind of situation an error. Here's an example of code in which an expression with no l-value is used as an argument that is passed by reference (the example was actually a Fortran program, but Java-like syntax is used here):
void mistake(int x) { // x is a reference parameter x = x+1; } void main() { int a; mistake(1); a = 1; print(a); }When this program was compiled and executed, the output was 2! That was because the Fortran compiler stored 1 as a literal at some address and used that address for all the literal "1"s in the program. In particular, that address was passed when "mistake" was called, and was also used to fetch the value to be assigned into variable a. When "mistake" incremented its parameter, the location that held the value 1 was incremented; therefore, when the assignment to a was executed, the location no longer held a 1, and so a was initialized to 2!
To understand why reference parameters are useful, remember that, although in Java all non-primitive types are really pointers, that is not true in other languages. For example, consider the following C++ code:
class Person { public: String name; int age; }; void birthday(Person per) { per.age++; } void main() { Person P; P.age = 0; birthday(P); print(P.age); }Note that in main, variable P is a Person, not a pointer to a Person; i.e., main's activation record has space for P.name and P.age. Parameter per is passed by value (there is no ampersand), so when birthday is called from main, a copy of variable P is made (i.e., the values of its name and age fields are copied into birthday's AR). It is only the copy of the age field that is updated by birthday, so when the print statement in main is executed, the value that is output is 0.
This motivates some reasons for using reference parameters:
Consider writing a method to sort the values in an array of integers. An operation that is used by many sorting algorithms is to swap the values in two array elements. This might be accomplished using a swap method:
static void swap(int x, int y) { int tmp = x; x = y; y = tmp; }Assume that A is an array of 4 integers. Draw two pictures to illustrate what happens when the call:
swap(A[0], A[1]);is executed, first assuming that this is Java code (all parameters are passed by value), and then assuming that this is some other language in which parameters are passed by reference. In both cases, assume that the array itself is stored in the heap (i.e., the space for A in the calling method's AR holds a pointer to the space allocated for the array in the heap). Your pictures should show the ARs of the calling method and method swap.
It is important to realize that the code generator will generate different code for a use of a parameter in a method, depending on whether it is passed by value or by reference. If it is passed by value, then it is in the called method's AR (accessed using an offset from the FP). However, if it is passed by reference, then it is in some other storage (another method's AR, or in the static data area). The value in the called method's AR is the address of that other location.
To make this more concrete, assume the following code:
void f(int a) { a = a - 5; } void main() { int x = 10; f(a); }Below is the code that would be generated for the statement a = a - 5, assuming (1) that a is passed by value and (2) assuming that a is passed by reference:
Passed by Value
Passed by Reference
Notice that when passing by reference the cell at address FP contains the address of a and not its value. Therefore, the first instruction will copy the location of a into t0 and the second instruction will extract the value at such a location and copy it into t1. Finally, the last instruction will copy the result into the location pointed by t0-i.e., the location of a.
The code generator will also generate different code for a method call depending on whether the arguments are to be passed by value or by reference. Consider the following code:
int x, y; x = y = 3; f(x, y);Assume that f's first parameter is passed by reference, and that its second parameter is passed by value. What code would be generated to fill in the parameter fields of f's AR?
Value-result parameter passing was used in Fortran IV and in Ada. The idea is that, as for pass-by-value, the value (not the address) of the actual parameters are copied into the called method's AR. However, when the called method ends, the final values of the parameters are copied back into the arguments. Value-result is equivalent to call-by-reference except when there is aliasing (note: "equivalent" means the program will produce the same results, not that the same code will be generated).
Two expressions that have the same l-value are called aliases. Aliasing can happen:
Pointer manipulation can create aliases, as illustrated by the following Java code. (Note: this kind of aliasing does not make pass-by-reference different from pass-by-value-result; it is included here only for completeness of the discussion of aliasing.)
Person p, q; p = new Person(); q = p; // now p.name and q.name are aliases (they both refer to the same location) // however, p and q are not aliases (they refer to different locations)
Pictorially:
This way of creating aliases (and the difference between reference parameters and value-result parameters in the presence of this kind of aliasing) are illustrated by the following C++ code:
int x = 1; // a global variable void f(int & a) { a = 2; // when f is called from main, a and x are aliases x = 0; } main() { f(x); cout << x; }As stated above, passing parameters by value-result yields the same results as passing parameters by reference except when there is aliasing. The above code will print different values when f's parameter is passed by reference than when it is passed by value-result. To understand why, look at the following pictures, which show the effect of the code on the activation records (only variables and parameters are shown in the ARs, and we assume that variable x is in the static data area):
Call-by-reference |
Call-by-value |
At time of call | |
After a = 2 | |
After x = 0 | |
After call When f returns the final value of value-result parameter a is copied back into the space for x, so: | |
Output | |
0 |
2 |
Consider the following C++ code:
void f(int &a, &b) { a = 2; b = 4; } main() { int x; f(x, x); cout << x; }Assume that f's parameters are passed by reference. In this case, when main calls f, a and b are aliases. As in the previous example, different output may be produced in this case than would be produced if f's parameters were passed by value-result (in which case, no aliases would be created by the call to f, but there would be a question as to the order in which values were copied back after the call). Here are pictures illustrating the difference:
Call-by-reference |
Call-by-value-result |
At time of call | |
After a = 2 | |
After b = 4 | |
After call | |
Output | |
With value-result parameter passing, the value of x after the call is undefined, since it is unknown whether a or b gets copied back into x first. This may be handled in several ways:
Assume that all parameters are passed by value-result.
Question 1: Give a high-level description of what the code generator must do for a method call.
Question 2: Give the specific code that would be generated for the call shown below, assuming that variables x and y are stored at offsets -8 and -12 in the calling method's AR.
int x, y; f(x, y);
Call-by-name parameter passing was used in Algol. The way to understand it (not the way it is actually implemented) is as follows:
Call-by-name parameter passing is not really implemented like macro expansion however; it is implemented as follows. Instead of passing values or addresses as arguments, a method (actually the address of a method) is passed for each argument. These methods are called 'thunks'. Each 'thunk' knows how to determine the address of the corresponding argument. So for the above example:
For the example above, call-by-reference would execute A[0] = 0 ten times, while call-by-name initializes the whole array!
The effect of evaluating argument expressions in the callee as needed can have some surprising effects. For example, an argument that would otherwise cause a runtime crash (say divide-by-zero) won't cause any problems until it is actually used (if at all). Factors like these often make call-by-name programs hard to understand - it may require looking at every call of a method to figure out what that method is doing.
Call-by-name is interesting for historical, research, and academic reasons, However, it is considered too confusing for developers in practice and industry has largely passed it by in favor of call-by-value or call-by-reference.
Comparisons of These Parameter Passing Mechanisms
Here are some advantages of each of the parameter-passing mechanisms discussed above:
Call-by-Value (when not used to pass pointers)
x = 0; f(x); {call-by-value so x not changed} z = x + 1; {can replace by z = 1 when optimizing}
Call-by-Reference
Call-by-Value-Result
Call-by-Name