4.5 方法参数
让我们回顾一下计算机科学术语,它描述了如何用编程语言将参数传递给方法(或函数)。值调用一词意味着该方法只获取调用方提供的值。相反,引用调用意味着该方法获取调用方提供的变量的位置。因此,方法可以修改存储在通过引用传递的变量中的值,但不能修改存储在通过值传递的变量中的值。这些“由什么调用”术语是描述各种编程语言中的方法参数的行为的标准计算机科学术语,而不仅仅是Java。(也有一个以名字命名的调用,主要是出于历史的兴趣,它被用于最古老的高级语言之一Algol编程语言中。)
Java编程语言总是使用按值调用。这意味着该方法获取所有参数值的副本。特别是,该方法不能修改传递给它的任何参数变量的内容。
例如,考虑以下调用:
double percent = 10;
harry.raiseSalary(percent);
无论方法是如何实现的,我们都知道在方法调用之后,percent的值仍然是10。
让我们更仔细地看看这种情况。假设一个方法试图使方法参数的值增加三倍:
public static void tripleValue(double x) // doesn't work
{
x = 3 * x;
}
然后我们调用这个方法:
double percent = 10;
tripleValue(percent);
但是,这不起作用。方法调用之后,percent的值仍然是10。以下是发生的情况:
- x是用percent(即10)的副本初始化的。
- x是三倍,现在是30。但percent仍然是10(见图4.6)。
图4.6 修改数字参数没有持久的效果。 - 方法结束,参数变量x不再使用。
但是,有两种方法参数:
- 基本类型(数字、布尔值)
- 对象引用
您已经看到,方法无法更改基元类型参数。对象参数的情况不同。您可以很容易地实现一种将员工工资增加三倍的方法:
public static void tripleSalary(Employee x) // works
{
x.raiseSalary(200);
}
当你调用
harry = new Employee(. . .);
tripleSalary(harry);
然后会发生以下情况:
- x初始化为harry值的副本,即对象引用。
raiseSalary
方法应用于该对象引用。x和harry两个变量都将工资提高了200%。- 方法结束,参数变量x不再使用。当然,对象变量harry继续引用薪水增加了三倍的对象(见图4.7)。
图4.7 修改对象参数具有持久的效果。
正如您所看到的,实现更改对象参数状态的方法是很容易实现的,实际上也是非常常见的。原因很简单。方法获取对象引用的副本,原始对象和副本都引用同一对象。
许多编程语言(特别是C++和Pascal)有两种参数传递机制:按值调用和引用调用。一些程序员(不幸的是,甚至一些书作者)声称Java使用对象的引用。那是假的。由于这是一个常见的误解,有必要详细研究一个反例。
让我们尝试编写一个交换两个Employee对象的方法:
public static void swap(Employee x, Employee y) // doesn't work
{
Employee temp = x;
x = y;
y = temp;
}
如果Java使用对象引用,那么该方法将工作:
var a = new Employee("Alice", . . .);
var b = new Employee("Bob", . . .);
swap(a, b);
// does a now refer to Bob, b to Alice?
但是,该方法实际上不会更改存储在变量a和b中的对象引用。swap方法的x和y参数是用这些引用的副本初始化的。然后,该方法继续交换这些副本。
// x refers to Alice, y to Bob
Employee temp = x;
x = y;
y = temp;
// now x refers to Bob, y to Alice
但归根结底,这是一种浪费的努力。当方法结束时,参数变量x和y将被放弃。原始变量a和b仍然引用与方法调用之前相同的对象(参见图4.8)。
图4.8 交换对象参数没有持久效果。
这表明Java编程语言不使用对象的调用。相反,对象引用是按值传递的。
下面是Java中的方法参数可以做什么和不能做什么的摘要:
- 方法无法修改基元类型的参数(即数字或布尔值)。
- 方法可以更改对象参数的状态。
- 方法不能使对象参数引用新对象。
清单4.4中的程序演示了这些事实。程序首先尝试将数字参数的值增加三倍,但没有成功:
Testing tripleValue:
Before: percent=10.0
End of method: x=30.0
After: percent=10.0
然后,它成功地将员工的工资提高了三倍:
Testing tripleSalary:
Before: salary=50000.0
End of method: salary=150000.0
After: salary=150000.0
方法之后,harry所指对象的状态发生了变化。这是可能的,因为该方法通过对象引用的副本修改了状态。
最后,程序演示了swap方法的失败:
Testing swap:
Before: a=Alice
Before: b=Bob
End of method: x=Bob
End of method: y=Alice
After: a=Alice
After: b=Bob
如您所见,参数变量x和y被交换,但变量a和b不受影响。
C++注意
C++既有按值调用,也有按引用调用。使用&,标记引用参数。例如,您可以轻松地实现方法
void tripleValue(double& x)
或void swap(Employee& x, Employee& y)
,这些方法可以修改其引用参数。
清单4.4 ParamTest/ParamTest.java
/**
* This program demonstrates parameter passing in Java.
* @version 1.01 2018-04-10
* @author Cay Horstmann
*/
public class ParamTest
{
public static void main(String[] args)
{
/*
* Test 1: Methods can't modify numeric parameters
*/
System.out.println("Testing tripleValue:");
double percent = 10;
System.out.println("Before: percent=" + percent);
tripleValue(percent);
System.out.println("After: percent=" + percent);
/*
* Test 2: Methods can change the state of object parameters
*/
System.out.println("\nTesting tripleSalary:");
var harry = new Employee("Harry", 50000);
System.out.println("Before: salary=" + harry.getSalary());
tripleSalary(harry);
System.out.println("After: salary=" + harry.getSalary());
/*
* Test 3: Methods can't attach new objects to object parameters
*/
System.out.println("\nTesting swap:");
var a = new Employee("Alice", 70000);
var b = new Employee("Bob", 60000);
System.out.println("Before: a=" + a.getName());
System.out.println("Before: b=" + b.getName());
swap(a, b);
System.out.println("After: a=" + a.getName());
System.out.println("After: b=" + b.getName());
}
public static void tripleValue(double x) // doesn't work
{
x = 3 * x;
System.out.println("End of method: x=" + x);
}
public static void tripleSalary(Employee x) // works
{
x.raiseSalary(200);
System.out.println("End of method: salary=" + x.getSalary());
}
public static void swap(Employee x, Employee y)
{
Employee temp = x;
x = y;
y = temp;
System.out.println("End of method: x=" + x.getName());
System.out.println("End of method: y=" + y.getName());
}
}
class Employee // simplified Employee class
{
private String name;
private double salary;
public Employee(String n, double s)
{
name = n;
salary = s;
}
public String getName()
{
return name;
}
public double getSalary()
{
return salary;
}
public void raiseSalary(double byPercent)
{
double raise = salary * byPercent / 100;
salary += raise;
}
}