在Java面试和日常开发中,“Java是值传递还是引用传递?”是一个经典问题。很多开发者对此感到困惑,但答案是唯一的:Java永远是值传递(Pass by Value)

为了彻底厘清这个概念,让我们首先将核心结论放在最前面:

核心结论

  1. 当传入的参数是 原始数据类型 (int, double 等) 或 不可变的引用类型 (如 String, Integer) 时:

    • 无论方法内部对这个参数做什么操作,都不会影响到方法外部的原始变量。
  2. 当传入的参数是 可变的引用类型 (如 List, Map 等) 时:

    • 如果修改的是对象自身的状态 (例如调用 list.add(item)),这个改变对方法外部是 可见的

    • 如果将参数引用指向一个新对象 (例如 list = new ArrayList<>()),这个改变对方法外部是 不可见的

为什么会这样?接下来,我们将通过代码示例和原理解析,深入探讨背后的一切。

什么是值传递 (Pass by Value)?

在程序设计语言中,方法调用时传递参数的方式主要有两种:值传递和引用传递。

  • 值传递 (Pass by Value):在调用方法时,实际上传递的是实参的一个副本 (copy)。方法内对形参的任何操作,都只是在操作这个副本,不会影响到方法外部的实参。
  • 引用传递 (Pass by Reference):在调用方法时,实际上传递的是实参的内存地址本身。方法内对形参的操作,会直接影响到该内存地址所指向的原始数据。

Java采用的是前者——值传递。理解的关键在于,我们需要搞清楚到底是什么“值”被复制并传递了。

  • 对于原始数据类型,传递的是值的副本
  • 对于引用类型,传递的是引用的副本(也就是内存地址的副本)

正是这个“引用的副本”让很多人误以为是“引用传递”。接下来我们用实例来验证我们的结论。

案例一:原始数据类型

这是最简单的情况。当我们将一个int类型的变量传入方法时,方法得到的是这个int值的一个完整拷贝。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class PassByValueExample {

public static void main(String[] args) {
int num = 10;
System.out.println("方法调用前: " + num); // 输出: 10
modify(num);
System.out.println("方法调用后: " + num); // 输出: 10
}

public static void modify(int value) {
value = 20;
System.out.println("方法内部: " + value); // 输出: 20
}
}

结果分析:

  • main方法中的num值为10。
  • 调用modify(num)时,Java创建了num的一个副本(也就是10这个值),并将这个副本交给了modify方法的参数value
  • 方法内部,value = 20这行代码修改的仅仅是value这个副本,与main方法中的num毫无关系。
  • 方法执行完毕后,main方法中的num依然是10。这完美印证了我们的第一个结论。

案例二:不可变的引用类型 (String)

String是一个特殊的引用类型,因为它是不可变的 (Immutable)。这意味着一旦一个String对象被创建,它的值就不能被改变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class PassByValueExample {

public static void main(String[] args) {
String message = "Hello";
System.out.println("方法调用前: " + message); // 输出: Hello
modify(message);
System.out.println("方法调用后: " + message); // 输出: Hello
}

public static void modify(String text) {
// "Hello" + ", World" 会创建一个全新的String对象
text = text + ", World";
System.out.println("方法内部: " + text); // 输出: Hello, World
}
}

结果分析:

  • main方法中的message是一个引用,它指向堆内存中值为”Hello”的字符串对象。
  • 调用modify(message)时,Java复制了这个引用(内存地址),并将引用的副本交给了modify方法的参数text。此时,messagetext指向同一个”Hello”对象。
  • 在方法内部,text = text + ", World"这行代码执行时,由于String的不可变性,它并不会修改”Hello”对象。相反,它创建了一个全新的字符串对象”Hello, World”,并将text这个引用指向了这个新对象。
  • 重要的是,只有text(引用的副本)的指向改变了,main方法中的message(原始引用)仍然指向最初的”Hello”对象。
  • 因此,方法调用后,message的值没有改变。

案例三:可变的引用类型 (List)

这是最能体现Java值传递精髓的地方,也是最容易产生困惑的地方。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import java.util.ArrayList;
import java.util.List;

public class PassByValueExample {

public static void main(String[] args) {
List<String> myList = new ArrayList<>();
myList.add("A");
myList.add("B");

System.out.println("方法调用前: " + myList); // 输出: [A, B]

// 场景1: 修改对象自身的状态
modifyState(myList);
System.out.println("modifyState 调用后: " + myList); // 输出: [A, B, C]

// 场景2: 将参数引用指向一个新对象
reassignReference(myList);
System.out.println("reassignReference 调用后: " + myList); // 输出: [A, B, C]
}

/**
* 场景1: 通过引用的副本,修改了引用所指向的堆内存中的对象内容
*/
public static void modifyState(List<String> list) {
list.add("C");
System.out.println("方法内部 (modifyState): " + list); // 输出: [A, B, C]
}

/**
* 场景2: 让引用的副本指向了一个全新的对象
*/
public static void reassignReference(List<String> list) {
list = new ArrayList<>(); // list这个引用副本指向了新对象
list.add("D");
System.out.println("方法内部 (reassignReference): " + list); // 输出: [D]
}
}

结果分析:

  1. modifyState 方法(修改对象状态)

    • 调用modifyState(myList)时,list参数得到了myList引用的一个副本。它们都指向同一个ArrayList对象。
    • 在方法内部执行list.add("C")时,它通过这个引用的副本找到了堆内存中的ArrayList对象,并修改了这个对象自身的状态(添加了一个元素”C”)。
    • 由于main方法中的myList和方法中的list指向的是同一个对象,所以这个修改对于main方法是可见的myList的内容变成了[A, B, C]
  2. reassignReference 方法(引用重新赋值)

    • 调用reassignReference(myList)时,list参数同样得到了myList引用的一个副本。
    • 关键在于list = new ArrayList<>()这行代码。它创建了一个全新的、空的ArrayList对象,并将方法内部的list这个引用副本指向了这个新对象。
    • 此时,main方法中的myList引用仍然指向旧的、包含[A, B, C]的那个对象,而方法内的list引用已经和它分道扬镳了。
    • 之后对list的任何操作(如list.add("D"))都只影响这个新对象,与main方法中的myList无关。
    • 因此,方法结束后,myList的值没有改变,依然是[A, B, C]

总结

希望通过以上的分析和示例,你能够清晰地理解:

Java 只有值传递。

  • 当参数是原始类型时,传递的是值的拷贝,方法内的修改不影响外部。
  • 当参数是引用类型时,传递的是引用的拷贝。因为拷贝的引用和原始的引用指向同一个对象,所以通过拷贝的引用去修改对象的状态(比如list.add()),会影响到原始引用。但是,如果让拷贝的引用去指向一个新对象(比如list = new ArrayList<>()),这并不会影响原始的引用。

只要牢牢记住开篇的两个核心结论,并理解其背后的“引用副本”原理,你就再也不会被这个问题所困扰了。