这是一个非常经典且重要的面试题,很多初学者会感到困惑,核心要点是:Java 中只有值传递,没有引用传递。 String 作为一种特殊的对象,其传递方式也遵循这个原则。

为了彻底理解,我们需要分三步走:
- 理解 Java 的核心机制:值传递
- 分析 String 的特殊性:不可变性
- 结合实例,看穿 String 传递中的“假象”
Java 的核心机制:值传递
我们必须明确一个概念:在 Java 中,参数传递的方式永远是值传递。
什么是“值传递”?
值传递意味着,当你调用一个方法并传递一个参数时,你传递的是该参数值的一个副本,而不是参数本身。

这个“值”是什么,取决于参数的类型:
-
对于基本数据类型(如
int,double,char等):- 传递的是值的副本。
- 在方法内部对这个副本的任何修改,都不会影响到方法外部的原始变量。
public class TestPrimitive { public static void main(String[] args) { int num = 10; System.out.println("调用方法前: num = " + num); // 输出 10 changeValue(num); System.out.println("调用方法后: num = " + num); // 输出 10 (没有改变) } public static void changeValue(int x) { x = 20; // 修改的是传递进来的副本 x,不是原始的 num System.out.println("方法内部: x = " + x); // 输出 20 } }结果分析:
changeValue方法里的x是num的一个副本,修改x对num毫无影响。 -
对于引用数据类型(如对象、数组):
(图片来源网络,侵删)- 传递的是引用地址的副本。
- 引用地址指向了对象在堆内存中的实际位置。
- 在方法内部,你可以通过这个地址的副本,去操作堆内存中那个唯一的对象,这会导致你在方法内对对象状态的改变,会影响到方法外部的原始对象。
- 如果你在方法内部重新赋值这个引用(即让它指向一个新的对象),那么改变的只是这个副本的地址,原始的引用变量并不会受影响。
public class TestObject { public static void main(String[] args) { Person p = new Person("张三"); System.out.println("调用方法前: p.getName() = " + p.getName()); // 输出 张三 changeReference(p); System.out.println("调用方法后: p.getName() = " + p.getName()); // 输出 李四 (对象内容被改变了) } // 修改对象的内容 public static void changeReference(Person person) { person.setName("李四"); // 通过引用副本,找到了堆中的同一个对象,并修改了它的状态 System.out.println("方法内部: person.getName() = " + person.getName()); // 输出 李四 } } class Person { private String name; public Person(String name) { this.name = name; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
String 的特殊性:不可变性
理解了值传递后,我们来看 String 为什么这么特殊。
String 在 Java 中是一个不可变类,这意味着,一旦一个 String 对象被创建,它的内容(指向的字符序列)就不能被改变。
任何看似修改了 String 的操作,s = s + " world";,其背后都发生了以下事情:
- 创建新对象:JVM 会在堆内存中创建一个新的
String对象,内容为 "hello world"。 - 重新引用:将变量
s(无论是原始变量还是方法内的副本)的引用,从原来指向 "hello" 的对象,改为指向这个新创建的 "hello world" 对象。 - 旧对象等待回收:原来那个 "hello" 的对象,因为没有任何引用指向它,就变成了垃圾,等待垃圾回收器来回收。
不可变性是理解 String 行为的关键。
String 值传递实例分析
我们把前两个概念结合起来,通过几个例子来彻底搞懂。
示例 1:在方法内修改 String 的内容(看似修改,实则无效)
public class StringTest1 {
public static void main(String[] args) {
String s = "hello";
System.out.println("调用方法前: s = " + s); // 输出 hello
modifyString(s);
System.out.println("调用方法后: s = " + s); // 输出 hello (没有改变)
}
public static void modifyString(String str) {
// str 是 s 的一个引用地址副本
// str += " world"; 这行代码做了什么?
// 1. 创建了一个新的 String 对象 "hello world"
// 2. 让 str 这个副本指向这个新对象
// 3. 原始的 s 仍然指向 "hello" 这个旧对象
str += " world";
System.out.println("方法内部: str = " + str); // 输出 hello world
}
}
结果分析:
main方法中的s和modifyString方法中的str,在初始时都指向堆内存中同一个 "hello" 对象。- 在
modifyString内部,str += " world";操作,由于 String 的不可变性,并没有修改 "hello" 对象,而是创建了一个新的对象 "hello world",并让str指向它。 - 这个操作只改变了引用副本
str的指向,并没有改变原始引用s的指向。 main方法中的s依然指向 "hello",输出结果不变。
示例 2:在方法内重新赋值 String(更清晰地展示引用副本)
public class StringTest2 {
public static void main(String[] args) {
String s = "hello";
System.out.println("调用方法前: s = " + s); // 输出 hello
reassignString(s);
System.out.println("调用方法后: s = " + s); // 输出 hello (没有改变)
}
public static void reassignString(String str) {
// str 是 s 的一个引用地址副本
// str = "new string"; 这行代码做了什么?
// 1. 创建了一个新的 String 对象 "new string"
// 2. 让 str 这个副本指向这个新对象
// 3. 原始的 s 仍然指向 "hello" 这个旧对象
str = "new string";
System.out.println("方法内部: str = " + str); // 输出 new string
}
}
结果分析:
这个例子和上一个本质完全一样,但 reassignString 的操作更纯粹——它就是直接让引用副本 str 指向一个全新的对象,这能让你更清楚地看到,改变的只是副本,原始变量 s 毫发无损。
示例 3:通过 String 的方法修改内容(不会改变原对象)
public class StringTest3 {
public static void main(String[] args) {
String s = "hello";
System.out.println("调用方法前: s = " + s); // 输出 hello
changeString(s);
System.out.println("调用方法后: s = " + s); // 输出 hello (没有改变)
}
public static void changeString(String str) {
// str.replace('l', 'p') 会返回一个新的 String 对象 "heppo"
// 它不会修改 str 原来指向的 "hello" 对象
str = str.replace('l', 'p');
System.out.println("方法内部: str = " + str); // 输出 heppo
}
}
结果分析:
String.replace()方法是返回一个新的字符串,而不是在原字符串上进行修改。str = str.replace(...);这行代码,同样是让引用副本str指向这个由replace方法生成的新对象 "heppo"。- 原始的
s依然指向那个未被修改过的 "hello" 对象。
总结与对比
为了让你看得更明白,我们用一个表格来总结:
| 场景 | 传递方式 | 方法内操作 | 对原始变量/对象的影响 | 原因 |
|---|---|---|---|---|
基本数据类型 (int) |
值传递(值的副本) | 修改副本的值 | 不受影响 | 方法内操作的是值的副本,与原始变量无关。 |
普通可变对象 (ArrayList) |
值传递(引用地址的副本) | 通过副本修改对象状态(如 add()) |
受影响 | 副本和原始引用指向同一个对象,修改对象内容是可见的。 |
普通可变对象 (ArrayList) |
值传递(引用地址的副本) | 重新赋值副本(如 list = new ArrayList()) |
不受影响 | 副本指向了新对象,原始引用仍指向旧对象。 |
不可变对象 (String) |
值传递(引用地址的副本) | 任何看似“修改”的操作(如 , replace()) |
不受影响 | 任何修改操作都会创建新对象,并让副本指向它,原始引用不变。 |
- Java 只有值传递。
- 对于
String这种引用类型,传递的是引用地址的一个副本。 - 由于
String的不可变性,任何在方法内部对String对象的“修改”,实际上都是创建了一个新的String对象,并让方法内的引用副本指向了这个新对象。 - 这个操作不会改变方法外部原始
String变量的引用,因此原始String的值保持不变。
当你下次被问到“Java 中 String 是值传递还是引用传递”时,你可以自信地回答: “Java 中只有值传递,对于 String 这种引用类型,传递的是其引用地址的副本,但由于 String 是不可变的,任何对它的‘修改’操作都会创建新对象,所以看起来像是原始值没有被改变,但这恰恰是值传递和不可变性共同作用的结果。”
