一、Java 只有值传递

Java 中方法参数的传递方式只有值传递(Pass by Value),但根据参数类型不同,传递的内容有所区别:

  • 基本数据类型(int、double 等):传递的是数值的副本。
  • 引用数据类型(String、List、对象等):传递的是引用的副本。

引用可以简单理解为指向对象的地址(类似 C/C++ 的指针,但不一定是真实的内存地址)。

例如:

List<String> list = new ArrayList<>();
change(list);

假设 ArrayList 对象位于 0x100,则参数传递过程可以理解为:

main 方法:
list = 0x100 ─────┐
                  ├──→ ArrayList 对象
change 方法:     │
x    = 0x100 ─────┘

list 和 x 是两个独立的引用变量,只不过它们保存了相同的引用值,指向同一个对象。

二、为什么 List 在方法中修改后,外面也会改变?

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

    change(list);

    System.out.println(list); // [A, B]
}

public static void change(List<String> x) {
    x.add("B");
}

原因是 x.add("B") 修改的是引用指向的 ArrayList 对象,而不是引用变量本身。

因此,list 和 x 指向的同一个对象发生了变化,外面的 list 自然能观察到。

但是,如果重新给参数赋值:

public static void change(List<String> x) {
    x = new ArrayList<>();
    x.add("B");
}

外面的 list 不会改变。

因为 x = new ArrayList<>() 只是让局部变量 x 指向一个新对象,原来的 list 仍然指向旧对象。

三、为什么 String 在方法中修改后,外面不会改变?

String 同样是引用类型,但它是不可变对象(Immutable Object)。

例如:

public static void main(String[] args) {
    String str = "ABC";
    change(str);
    System.out.println(str); // ABC
}

public static void change(String s) {
    s = "DEF";
}

假设 "ABC" 对象的地址为 0x100,"DEF" 对象的地址为 0x200。

参数刚传入时:

str = 0x100 ────→ "ABC"
s   = 0x100 ────→ "ABC"

执行 s = "DEF" 后:

str = 0x100 ────→ "ABC"
s   = 0x200 ────→ "DEF"

这里并没有修改原来的 "ABC" 对象,而是修改了局部引用变量 s 的指向。

而且 String 不可变,concat()、replace() 等方法不会修改原来的字符串对象,而是返回结果字符串。

public static void change(String s) {
    s = s.concat("DEF");
}

同样不会影响外面的 str。

四、String 和 StringBuilder 的区别

public static void main(String[] args) {
    String str = "ABC";
    StringBuilder sb = new StringBuilder("ABC");

    change1(str);
    change2(sb);

    System.out.println(str); // ABC
    System.out.println(sb);  // ABCDEF
}

public static void change1(String s) {
    s = s + "DEF";
}

public static void change2(StringBuilder s) {
    s.append("DEF");
}

原因:

  • String 不可变,s + "DEF" 得到新的字符串,再让局部变量 s 指向它。
  • StringBuilder 可变,append() 直接修改原对象,因此外部可以观察到变化。

五、总结

操作 是否影响外面的对象或变量 原因
int x = 20 否 修改基本类型参数的副本
list.add("A") 是 修改共同指向的对象
list.clear() 是 修改共同指向的对象
list = new ArrayList<>() 否 只改变局部引用的指向
str = "DEF" 否 只改变局部引用的指向
str = str.concat("DEF") 否 结果重新赋给局部引用
sb.append("DEF") 是 修改原 StringBuilder 对象

核心结论:

Java 只有值传递。引用类型传递的是引用的副本,而不是对象的副本。

需要区分两种操作:

  1. 修改引用指向的对象: 如果对象可变,外部可以观察到对象内容的变化,例如 list.add()。
  2. 修改引用变量的指向: 不会影响外部引用,例如 list = new ArrayList<>()、str = "DEF"。

String 与 List 的传参机制完全相同,区别仅在于 String 是不可变对象(其实就是说你不能改变String对象的某部分,你修改、赋值等返回的都是一个新对象),而 List 通常是可变对象(就是你可以修改、删除List中的某个值啊,这个List对象还是原来的对象)。