5.3 参数、返回值与值传递
前置知识:会声明和调用方法
预计时间:35 分钟
完成标志:能用基本类型和引用类型实验证明 Java 只有值传递
形参接收实参求值结果的副本
static void buildItem(String name, int days) {
System.out.println(name + ": " + days + " days");
}
buildItem("standard item", 3);name、days 是形参。字符串表达式与整数表达式是实参。Java 从左到右求值实参,把结果值复制给新方法调用中的形参变量。
基本类型值传递
static void increase(int value) {
value++;
System.out.println("inside=" + value);
}
int count = 10;
increase(count);
System.out.println("outside=" + count);输出:
inside=11
outside=10形参 value 得到整数 10 的副本,修改它不修改调用者变量 count。
引用类型仍然值传递
static void changeFirst(int[] values) {
values[0] = 99;
}
int[] numbers = {1, 2, 3};
changeFirst(numbers);
System.out.println(numbers[0]); // 99复制的是引用值。调用者变量和形参副本都能标识同一个数组对象,所以通过任一引用修改数组,另一方都能观察到。
这不等于“引用传递”。重新赋值形参不会改调用者变量:
static void replace(int[] values) {
values = new int[] {7, 8, 9};
values[0] = 100;
}
int[] numbers = {1, 2, 3};
replace(numbers);
System.out.println(numbers[0]); // 1不要把引用值直接称为可见内存地址。Java 语言只承诺它能标识对象或为 null,普通代码不能对引用做地址算术。
final 参数不等于不可变对象
static void update(final int[] values) {
values[0] = 42; // 合法:修改对象
// values = new int[1]; // 编译错误:不能重赋形参
}final 约束形参变量只赋值一次,不冻结数组内容。
返回一个结果,而不是试图改调用者局部变量
错误期待:
static void swap(int left, int right) {
int temporary = left;
left = right;
right = temporary;
}调用 swap(a, b) 不会交换调用者的两个局部变量。更清楚的设计是返回结果对象或数组;初学阶段可返回两元素数组:
static int[] swapped(int left, int right) {
return new int[] {right, left};
}
int[] result = swapped(3, 5);后面学习 record 后,可返回有字段名的结果类型,避免 result[0] 的位置含义。
返回值与失败契约
static int divide(int dividend, int divisor) {
if (divisor == 0) {
throw new IllegalArgumentException("divisor is zero");
}
if (dividend == Integer.MIN_VALUE && divisor == -1) {
throw new ArithmeticException("division overflows int");
}
return dividend / divisor;
}方法可能正常返回、抛异常,或因更严重错误突然结束。本例拒绝零除数和 MIN_VALUE / -1,其余输入按 Java 整数除法向零截断。文档不要只写“返回 int”,还要说明范围、单位和失败方式。
多个参数与参数对象
static int quote(String material, int kilograms,
boolean rush, int customerLevel) {
// ...
}参数多不自动等于设计错误,但连续出现多个相同类型、布尔开关或经常成组变化时,调用容易写错:quote("iron", 5, true, 2) 难以自解释。可引入有名字的请求对象,或拆分职责。
可变参数是数组语法糖
static int sum(int... values) {
int total = 0;
for (int value : values) {
total = Math.addExact(total, value);
}
return total;
}
System.out.println(sum(1, 2, 3));方法体把 values 当数组。调用者也可传 int[]。sum() 收到空数组并返回 0;sum((int[]) null) 收到的是 null,遍历时会抛 NullPointerException。无参数与传入 null 不是一回事。一个方法最多有一个可变参数,且必须位于参数列表末尾。重载解析会优先考虑适用的固定参数形式,再考虑可变参数阶段,混用时需要测试歧义。
Python 与 C++ 边界
Python 也会把对象引用绑定给新的形参名;可变对象能被共同观察,形参重新绑定不改变调用者名字。术语常被描述为 call by sharing,不能据此把 Java 称为引用传递。
C++ 明确区分按值、引用参数和指针。Java 没有能直接别名调用者局部变量的参数语法。
专业速查
| 现象 | 原因 |
|---|---|
| 修改基本形参,调用者不变 | 复制了基本值 |
| 修改数组元素,调用者可见 | 两个引用值标识同一可变对象 |
| 重新赋值数组形参,调用者不变 | 只改了形参副本 |
final 数组仍能改元素 | final 约束引用变量,不冻结对象 |
| 想返回多个结果 | 返回有明确结构的结果类型 |
离开这一页前
用 increase、changeFirst 和 replace 三个例子回答同一个问题:调用时究竟复制了什么值?数组元素可见变化来自共享对象,形参重新赋值不影响调用者变量;final 也只约束形参本身。返回值还要写清范围、单位和失败方式。
每次调用都会进入一层新的执行上下文。
规范入口:JLS §15.12.4.2:实参求值。