跳到主要内容
约 6 分钟3 张卡

Java 值传递

Java 方法调用只有值传递,也就是把实参当前保存的值复制给形参。本文先区分基本值、引用值和对象,再通过赋值与传参示例解释共享修改,最后说明 final 引用与浅拷贝。

1. 面试核心​

先记住这几条,再读正文
  • 变量保存的值。 基本类型变量保存基本值,引用变量保存引用值或 null。
  • 值复制。 Java 的赋值和传参都复制当前保存的值,引用类型复制的是引用值。卡 · 变量保存的值
  • 对象修改与重新赋值。 形参与实参指向同一对象时,修改对象对调用方可见,而重新给形参赋值只改变形参变量。卡 · 赋值与传参
  • final 引用。 final 引用初始化后保持指向不变,它指向的可变对象仍允许修改内容。卡 · final 引用与浅拷贝

2. 变量保存的值​

理解赋值前,要先区分基本值与引用值。基本类型变量保存具体值,比如 int 变量保存整数。下面把整数复制给另一个变量,再修改副本:

int original = 101;
int copied = original;
copied = 202;

System.out.println(original); // 101

copied 得到的是 101 这个值,之后给它赋值为 202,只会改变 copied。因此,original 仍然是 101。

引用类型变量保存的是用于访问对象的引用值,也可以保存 null。复制引用后,两个变量就能访问同一个对象:

User first = new User("Lin");
User second = first;

second.setName("Asuka");
System.out.println(first.getName()); // Asuka

first 和 second 是两个变量,但保存着指向同一个 User 的引用。因此,通过 second 修改对象姓名后,通过 first 也会读到 Asuka。这次操作改动的是共享对象的字段。

不要用“基本类型一定在栈、对象一定在堆”解释语言语义。JVM 可以优化实际存储位置,赋值和传参规则不会因此改变。

3. 赋值与传参​

修改对象字段与重新给引用变量赋值,会产生不同结果。先看重新赋值:它只改变当前变量保存的引用。

User first = new User("Lin");
User second = first;

second = new User("Asuka");

System.out.println(first.getName()); // Lin
System.out.println(second.getName()); // Asuka

second 重新指向新对象后,first 仍指向原来的 User,所以两次输出分别是 Lin 和 Asuka。

方法调用也遵循复制规则。调用处传入的值称为实参,方法声明中接收值的变量称为形参。每次调用都会创建新的形参变量,并用实参当前保存的值初始化:

static void rename(User user) {
user.setName("Asuka");
}

static void replace(User user) {
user = new User("Asuka");
}

假设调用方传入姓名为 Lin 的用户,两种操作的区别如下:

操作修改的内容调用方是否可见
user.setName(...)双方共同指向的对象可见
user = new User(...)形参变量保存的引用不可见

调用 rename 后,共享对象的姓名变为 Asuka,调用方能读到这个变化。单独调用 replace 则只改变局部形参 user,调用方仍指向姓名为 Lin 的原对象。

两种结果都来自引用值的复制。Java 没有让方法通过形参直接替换调用方变量的引用传递机制。

4. 数组与 String​

数组和 String 都是引用类型,传参时同样复制引用值。它们的差别在于内容是否允许修改,先看可以修改元素的数组:

static void update(int[] values) {
values[0] = 101;
values = new int[] {202};
}

int[] numbers = {1};
update(numbers);
System.out.println(numbers[0]); // 101

values[0] = 101 修改的是共享数组,因此调用方读到 101。随后给 values 赋一个新数组,只改变局部形参,numbers 仍指向原来的数组。

String 的内容不可变,所以示例中的拼接会得到另一个字符串,再把新引用赋给局部形参:

static void append(String value) {
value = value + " 101";
}

String title = "Coding";
append(title);
System.out.println(title); // Coding

value 得到了拼接后的新引用,调用方的 title 则保持原引用。因此,方法返回后仍输出 Coding,这与其他引用类型遵循相同的传参规则。

5. final 引用与浅拷贝​

限制变量重新赋值,与复制对象,是两种不同的操作。final 引用初始化后保持指向不变,但对象是否可修改仍由对象自身决定。数组的 clone() 则会创建新数组,下面一起观察它们对共享修改的影响:

final List<String> names = new ArrayList<>();
names.add("Lin"); // 合法:修改列表
// names = new ArrayList<>(); // 编译错误:重新赋值

StringBuilder[] first = {new StringBuilder("A")};
StringBuilder[] second = first.clone();
second[0].append("B");
System.out.println(first[0]); // AB

names.add("Lin") 修改列表内容,引用仍指向原列表,所以合法。注释中的赋值会改变 names 的指向,因此是编译错误。

数组克隆创建了新数组,但数组中的元素引用只是被复制。这称为浅拷贝:外层容器不同,内部对象仍然共享。所以,second[0].append("B") 修改了共享的 StringBuilder,通过 first[0] 也会读到 AB。

深拷贝还要复制可变子对象,才能隔离这些修改。存在多个引用共享同一个子对象时,还需定义复制后是否保留这种共享关系。

6. 常见问题​

6.1 为什么交换两个对象参数无法交换调用方变量?​

因为方法内交换的是两个形参保存的引用副本,改动只作用于局部变量。调用方的两个变量保持原来的引用,所以方法返回后仍访问原来的对象。

6.2 方法怎样返回多个结果?​

6.3 引用值就是内存地址吗?​

7 · 面试题卡片​

本页题目与参考回答

7.1 Java 是值传递还是引用传递​

出现公司:腾讯、网易、美团

考察重点

  • 区分基本类型保存的具体值与引用类型保存的引用值。
  • 根据复制后的引用关系,解释修改对象与重新赋值形参的不同结果。

相关内容:变量保存的值、赋值与传参。

参考回答

Java 只有值传递。

调用方法时,形参是新变量,由实参当前保存的值初始化。基本类型复制具体值,引用类型复制引用值。

复制引用后,形参和实参指向同一对象,所以通过形参修改字段会被调用方看到。重新给形参赋值则只改变局部变量,调用方保存的引用保持不变。

7.2 rename 与 replace 为什么产生不同结果​

出现公司:哈啰数科

考察重点

  • 说明修改对象状态与重新给引用变量赋值分别改变了什么。
  • 画出形参、实参与对象之间的关系,并据此推导调用后的结果。

相关内容:赋值与传参。

参考回答

rename 修改共享对象,replace 只改变形参的引用,所以调用方看到的结果不同。

两个方法收到的都是引用值副本,最初都与调用方指向同一个对象。

rename 通过这个副本修改姓名,因此调用方也能读到新姓名。replace 让局部形参指向另一个对象,而调用方变量仍保存原引用,所以原对象保持不变。

7.3 final 引用和数组 clone 能否隔离对象修改?​

出现公司:暂无已核验的公开来源(本站自测)

考察重点:解释 final 约束的是哪个变量,以及浅拷贝后哪些对象仍被共享。

相关内容:final 引用与浅拷贝。

参考回答

两者都无法隔离对共享可变对象的修改。

final 只约束变量重新赋值,所以 final 列表仍可以添加元素。

引用数组的 clone() 创建新数组,但复制的是元素引用,因此两个数组仍共享可变元素。例如,first[0] 指向内容为 A 的 StringBuilder,执行 second = first.clone() 后,再调用 second[0].append("B"),通过 first[0] 也会读到 AB。要隔离这类修改,还需复制内部的可变对象。

8. 参考资料​