引言:关于“按引用”与“按值”的永恒争论
如果你曾经用 Java 编程,并将数组或对象传递给方法,然后惊讶地发现原始数据发生了变化——你并不孤单。这个问题从 Java 语言诞生之初就一直困扰着新手(甚至不仅仅是新手)。在 Stack Overflow 上,它获得了数百万次浏览,而评论区的争论多年来从未平息。
今天,我们将一劳永逸地弄清楚:Java 是“按引用传递”还是“按值传递”? 剧透:答案很明确,但魔鬼总是藏在细节中。我们不仅会给出干巴巴的定义,还会通过实际示例展示 Java 中参数传递的工作原理,以及为什么许多人会感到困惑。
Java 的创造者怎么说?
官方文档以及 Java 的创造者 James Gosling 本人没有留下任何争论的余地:Java 始终按值传递参数。没有任何例外。无论是基本类型还是对象。
但为什么代码会改变对象?为什么传递给方法的 List 或数组会被修改?让我们来一探究竟。
按值传递:这到底意味着什么?
首先,给出一个明确的定义。按值传递 意味着在调用方法时,会创建变量值的副本,并将这个副本传递给方法。原始变量(在调用代码中)保持不变。
按引用传递 是指将原始变量的引用传递给方法,方法可以直接更改其值(例如,替换对象)。Java 中没有这种情况。
现在,来看示例。
示例 1:基本类型(int, double, boolean)
对于基本类型,一切都很明显。让我们编写一个简单的代码:
public class Main { public static void main(String[] args) { int x = 10; changePrimitive(x); System.out.println("After method x = " + x); // 10 }
public static void changePrimitive(int value) { value = 20; // 修改副本 }}输出:
After method x = 10一切都很合理:我们传递了数字 10 的副本,在方法内部将副本改为 20,但原始值保持不变。这是纯粹的按值传递。
示例 2:对象(字符串、列表、自定义类)
现在是最有趣的部分。请看:
public class Main { public static void main(String[] args) { StringBuilder sb = new StringBuilder("Hello"); changeStringBuilder(sb); System.out.println("After method: " + sb); // Hello World! }
public static void changeStringBuilder(StringBuilder builder) { builder.append(" World!"); // 修改对象的内容 }}输出:
After method: Hello World!“啊哈!”你可能会说,“对象被修改了!所以这是按引用传递!”
不。 这是 90% 的新手都会掉入的陷阱。让我们分析一下实际发生了什么。
变量 sb 中存储了什么?
在 Java 中,对象类型的变量(例如 StringBuilder、List、String)并不存储对象本身。它存储的是指向内存中对象的引用(地址)。对象本身位于堆(heap)中的某处。
当你将 sb 传递给方法时,你传递的是这个引用的副本。也就是地址的副本。原始引用和副本都指向同一个对象。因此,当你通过引用副本修改对象的内容时,这些修改也会通过原始引用可见。
但是!如果你尝试在方法内部将一个新对象赋值给参数,原始引用不会改变。我们来证明这一点:
public class Main { public static void main(String[] args) { StringBuilder sb = new StringBuilder("Hello"); changeReference(sb); S