Java 中到底是值传递还是引用传递?
Java 的参数传递机制一直是初学者和部分有经验开发者容易混淆的知识点。特别是在涉及对象、数组等引用类型参数时,“Java 是值传递还是引用传递?”常成为争议焦点。笔者将通过理论、代码、内存图和对比分析,帮助理解 Java 的参数传递机制,避免常见的trick。
一、基础概念:值传递与引用传递
值传递(Pass by Value):
调用方法时,实参的值会被复制一份传递给形参。方法内部对形参的修改不会影响外部实参。
引用传递(Pass by Reference):
调用方法时,传递的是实参的地址,形参和实参指向同一块内存。方法内部对形参的修改会影响外部实参。
举例说明:
- 值传递:
假设有变量 a=10,调用 foo(a) 时,foo 方法接收到 a 的副本,foo 内部怎么改都不会影响 main 方法中的 a。 - 引用传递:
假设有对象 obj,调用 foo(obj) 时,foo 内部和外部的 obj 指向同一个对象。foo 内部对 obj 的内容修改,main 方法中的 obj 也会感知到。
二、Java 的参数传递机制本质
1. Java 只支持值传递
Java 方法调用时,无论参数类型如何,传递的都是实参的副本。
- 对于基本类型,传递的是数值副本。
- 对于引用类型,传递的是“引用的副本”(即对象地址的副本)。
Java 没有 C++ 那样的引用传递语法。
2. 基本类型参数传递
Java 八种基本类型(byte、short、int、long、float、double、char、boolean)作为参数时,传递的是字面值的副本。
3. 引用类型参数传递
Java 的引用类型(对象、数组等)参数传递时,传递的是引用的副本,即对象在堆内存中的地址的副本。
形参和实参都指向同一个堆内存的对象,但形参本身的变化不会影响实参。
三、代码示例与内存图详解
1. 基本类型参数传递
public class BasicTypeDemo {
public static void main(String[] args) {
int num = 10;
modify(num);
System.out.println("main方法中的num: " + num); // 输出10
}
public static void modify(int n) {
n = 20;
System.out.println("modify方法中的n: " + n); // 输出20
}
}
分析:
modify(num)
时,num 的值被复制给 n。- n 的修改不会影响 main 方法中的 num。
内存示意图:
2. 引用类型参数传递
class Person {
String name;
}
public class ReferenceTypeDemo {
public static void main(String[] args) {
Person p = new Person();
p.name = "张三";
modify(p);
System.out.println("main方法中的p.name: " + p.name); // 输出李四
}
public static void modify(Person p) {
p.name = "李四";
System.out.println("modify方法中的p.name: " + p.name); // 输出李四
}
}
分析:
- main 方法和 modify 方法中的 p 都指向同一个 Person 对象。
- 修改 p.name,main 方法可见。
内存示意图:
3. 修改引用本身与修改对象内容
class Person {
String name;
}
public class ReferenceReassignDemo {
public static void main(String[] args) {
Person p = new Person();
p.name = "张三";
reassign(p);
System.out.println("main方法中的p.name: " + p.name); // 输出张三
}
public static void reassign(Person p) {
p = new Person(); // p 指向新对象
p.name = "李四";
System.out.println("reassign方法中的p.name: " + p.name); // 输出李四
}
}
分析:
- reassign 方法内部让 p 指向新对象,只影响方法内部。
- main 方法中的 p 仍指向原对象。
内存示意图:
4. 数组参数传递
public class ArrayDemo {
public static void main(String[] args) {
int[] arr = {1, 2, 3};
modify(arr);
System.out.println("main方法中的arr[0]: " + arr[0]); // 输出100
}
public static void modify(int[] array) {
array[0] = 100;
}
}
分析:
- 数组作为引用类型,array 和 arr 指向同一个数组对象。
- 修改 array[0],main 方法可见。
内存示意图:
四、深入剖析:引用类型传递的本质
1. 形参和实参的关系
- 方法调用时,实参的引用值(即地址)被复制给形参。
- 形参和实参“引用值”相同,但它们是两个独立的变量。
- 形参指向的对象内容被修改,外部可见;形参指向新的对象,外部不可见。
2. 代码实验:交换两个对象
class Wrapper {
int value;
}
public class SwapDemo {
public static void main(String[] args) {
Wrapper a = new Wrapper(); a.value = 1;
Wrapper b = new Wrapper(); b.value = 2;
swap(a, b);
System.out.println("a.value: " + a.value + ", b.value: " + b.value); // 1, 2
}
public static void swap(Wrapper x, Wrapper y) {
Wrapper temp = x;
x = y;
y = temp;
}
}
分析:
- swap 方法内部交换的是引用副本,不影响 main 方法中的 a、b 的引用。
- 如果修改 x.value/y.value,则外部可见。
五、一些小小的trick
1. 为什么很多人误以为 Java 是引用传递?
- 因为对象内容的修改外部可见,容易误以为是“引用传递”。
- 实际上,Java 传递的是引用的副本,不是引用本身。
2. 代码分解
分解1:如下代码输出什么?
public class Test {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder("Hello");
change(sb);
System.out.println(sb); // ?
}
public static void change(StringBuilder sb) {
sb.append(" World");
sb = new StringBuilder("Hi");
sb.append(" Java");
}
}
答案:
输出 Hello World
解释:
sb.append(" World")
改变了原对象内容,main 方法可见。sb = new StringBuilder("Hi")
只改变了 change 方法内 sb 的引用,不影响 main 方法的 sb。
分解2:如下代码输出什么?
public class Test {
public static void main(String[] args) {
int[] arr = {1, 2, 3};
change(arr);
System.out.println(arr[0]); // ?
}
public static void change(int[] arr) {
arr[0] = 100;
}
}
答案:
输出 100
解释:
- 数组作为引用类型,方法内部修改内容,外部可见。
六、对比其他语言参数传递机制
语言 | 基本类型参数 | 对象/引用类型参数 | 能否修改引用本身 | 能否修改内容 |
---|---|---|---|---|
Java | 值传递 | 引用值(地址)的副本 | 否 | 是 |
C++ | 值传递/引用传递/指针传递 | 引用传递/指针传递 | 是 | 是 |
Python | 引用的副本(值传递) | 引用的副本(值传递) | 否 | 是(可变对象) |
- Python 与 Java 类似,参数传递的是引用的副本。
- C++ 可以显式选择传递方式。
七、代码实践后的一些建议
- 理解参数传递机制,避免因误解导致 bug。
- 需要在方法内部修改引用本身时,可以通过返回新引用或使用包装类(如数组、集合等)实现。
- 初学 Java 实践时遇到相关问题,建议多用代码和绘制内存图解释,避免思维紊乱与描述不清。
八、结论
- Java 方法参数传递本质上都是值传递。
- 基本类型传递数值副本,引用类型传递引用(地址)的副本。
- 方法内部修改引用所指对象的内容会影响外部,修改引用本身不会影响外部。
- 正确理解 Java 参数传递机制,对写出高质量、高可维护性的 Java 程序至关重要。