Java 是值传递还是引用传递?

2026年 阅读约 7 分钟 面试指南 · Java面试

深入解析Java参数传递机制:为什么说Java只有值传递、方法内改对象内容生效但重新赋值无效、String与包装类的不可变陷阱、Integer缓存,分三层讲解面试高频考点。

一句话总结

Java 只有值传递:传基本类型拷贝"值",传对象拷贝"引用(地址)的值"。所以方法内能通过引用修改对象的内容(list.add 生效),但对形参重新赋值不影响外部(list = newList 无效)。String/包装类因不可变,怎么"改"都是换新对象,表现得像值传递。

初级理解

两个概念先分清:值传递(pass by value)传的是变量的副本,方法内改形参不影响实参;引用传递(pass by reference)传的是变量本身(别名),方法内改形参直接影响实参。C++ 的 & 才是引用传递,Java 没有这个语法。

// 基本类型:拷贝值,互不影响 void change(int x) { x = 100; } int a = 1; change(a); // a 还是 1 // 对象类型:拷贝"引用的值",两把钥匙开同一扇门 void modify(List<String> list) { list.add("new"); } List<String> l = new ArrayList<>(); modify(l); // l 里多了 "new"
经典比喻:传对象 = 复制了一把钥匙给对方。对方用钥匙能搬走屋里的家具(改对象内容),但对方把钥匙熔了重铸一把(形参重新赋值),你家门口那把原钥匙毫无变化。

中级深入

能改内容 vs 不能换引用,一个实验看清:

void test(StringBuilder sb, List<Integer> list) { sb.append(" world"); // ① 生效:通过引用改对象内容 sb = new StringBuilder(); // ② 无效:形参指向新对象,与外部无关 list = Arrays.asList(9); // ③ 无效:同上,只是换了形参指向 } StringBuilder s = new StringBuilder("hello"); List<Integer> l = new ArrayList<>(List.of(1)); test(s, l); System.out.println(s); // "hello world"(①生效) System.out.println(l); // [1](③没生效)

String 的"特殊"表现:String 是不可变类——所有看似修改的方法(concat/replace/substring)都返回新对象。所以方法内 str += "x" 改的是形参指向的新串,外部永远看不到变化,很多人因此误以为 String 是值传递——其实还是引用的值传递,只是对象本身不可变。

void append(String s) { s += " world"; } // 等价于 s = new StringBuilder(s).append(...) String str = "hello"; append(str); System.out.println(str); // 还是 "hello"

高级拓展

JVM 视角:方法调用时,实参的值从调用方的操作数栈复制到被调方法栈帧的局部变量表 slot 中。基本类型 slot 里就是值本身;引用类型 slot 里是 reference(指向堆中对象)。栈帧之间拷贝的永远是 slot 内容——这就是"只有值传递"在字节码层面的体现。

Integer 缓存陷阱(高频追问):Integer.valueOf 对 -128~127 返回缓存对象,这个范围内两个 Integer 用 == 比较是 true,范围外是 false。把它和"值传递"组合出题是面试经典:

void swap(Integer a, Integer b) { Integer t = a; a = b; b = t; // 无效:形参换指向 } // 进阶坑:想利用"改内容"绕过不可变?用反射改 Integer.value 字段 // 反射 + 缓存会把 1 和 2 一起改掉(同一个缓存对象),场面极度混乱 // ——面试说清楚"为什么会这样"比写这段代码重要

为什么"能改内容"有时也失效?——看被改的东西是否经过引用链:数组元素、List.add 是通过引用改堆内数据,生效;而 str = xxx、list = xxx 是改栈上 slot 的指向,无效。判断标准只有一条:改动发生在堆对象上还是栈变量上。

注意:自动装箱发生在"赋值/传参/运算"时刻。把 int 传给 Integer 参数会创建/获取包装对象,之后改的是包装对象——但由于不可变,外部依然无感。

实战场景

场景一:方法"回传"结果为什么常见返回值而不是改参数

// 反模式:指望方法内换引用影响外部 void fill(List<String> list) { list = queryData(); } // 无效! // 正确姿势一:返回新对象 List<String> fill() { return queryData(); } // 正确姿势二:保留引用,改内容 void fill(List<String> list) { list.addAll(queryData()); }

场景二:业务代码里"参数被下游改了"的防御

// 入参 List 被下游方法 add/.clear 污染 process(new ArrayList<>(inputList)); // 传入前拷贝 // 返回集合用 List.copyOf() 返回不可变视图,杜绝调用方误改 public List<Role> getRoles() { return List.copyOf(roles); }

场景三:Long 与 long 的 == 坑(值传递 + 缓存双重叠加)

Long a = 127L, b = 127L; System.out.println(a == b); // true(缓存内同一对象) Long c = 128L, d = 128L; System.out.println(c == d); // false(各自装箱新对象) System.out.println(c.equals(d)); // true(永远用 equals)

面试模拟

Q:既然传对象能改变外部对象,为什么还说 Java 只有值传递?

A:值传递/引用传递区分的是"传的变量本身还是变量的副本"。Java 传对象时复制的是引用变量的值(地址)——方法内对形参重新赋值不影响外部,这正是值传递的特征;引用传递(如 C++ 引用形参)中形参就是实参的别名,赋值会影响外部。所以准确说法是:Java 按值传递一切参数,对象传的是"引用的值"。

Q:方法内 String 拼接为什么外面看不到?StringBuilder 就能?

A:String 不可变,任何"修改"都产生新对象并让形参指向它,改动停留在栈变量上,外部无感;StringBuilder 可变,append 修改的是堆上同一个对象的内容,外部通过原引用自然看得到。区别不在传递机制,在对象是否可变。

Q:Integer 作为参数在方法内自增,外部为什么不变?

A:三层原因叠加——① Integer 不可变,没有自增操作,i++ 实际是"拆箱加一再装箱新对象";② 装箱后的新对象赋给了形参(栈上换指向),外部无感;③ 即使不谈赋值,-128~127 的缓存对象也是共享的,谁都不该被改。结论:跨方法传"会变的数"用自定义可变容器、数组元素或返回值,不要指望改包装类。