☰
Java参数传递详解:值传递与引用传递的底层原理
2026/10/17 3:22:41 网站建设 项目流程

1. 面试开场:为什么这个问题一上来就刷掉一半人

先直接给结论:Java 里的参数传递只有一种方式,那就是值传递。这句话我在面试现场已经听过很多次,但真正能把这句话讲明白的人不多。很多候选人能背出结论,却在一道 memo swap 的代码题上栽跟头,或者在追问“既然对象传的是引用,怎么能说是值传递”的时候就卡住了。这篇文章就围绕这个面试常考基础展开,把 Java 的传递方式从概念、原理到实战陷阱逐个拆开。

如果你正在准备面试,或者已经工作几年但一直没把这个问题彻底搞清楚,这篇文章值得看完。它能帮你回答的不只是“是值传递还是引用传递”这层题面,而是面试官藏在后面的层层追问:什么是实参和形参?引用变量在栈上还是堆上?修改对象字段和修改引用本身的区别到底是什么?String 为什么传进去改不动?这些搞清楚了,Java 传递方式这个考点才算真正过关。

我写这篇内容的思路也很简单:先讲清楚严格定义,再从 JVM 方法调用层面解释为什么只有值传递,然后给几个可以亲手复现的代码实验,最后整理成一套面试答题策略。全程不绕弯子,全部是能直接用的东西。

2. 先把概念掰清楚:值传递和引用传递到底指什么

2.1 形式参数、实际参数这两顶帽子先戴上

讨论传递方式之前,必须先分清两个基础概念:形式参数和实际参数。

方法定义的时候写在括号里的参数,叫形式参数,也就是形参。它本质上是一个占位符,不代表任何具体的值。方法调用的时候真正传进去的变量或者表达式,叫实际参数,也就是实参。实参是真正有数据的那个东西。

举个例子:

public void change(int x) { x = 100; } public static void main(String[] args) { int a = 5; new Demo().change(a); System.out.println(a); // 输出多少? }

这里的x就是形参,a就是实参。当我们调用change(a)时,真正进入方法体内的x其实是a的拷贝,不是a本身。很多人对传值传引用有误解,根源就在于没把这个最基础的关系搞清楚。

还有一个常见混淆点:方法定义里的形参名和实参名一样,或者一模一样,会让初学者误以为“它就是同一个变量”。比如下面这种:

public void set(int number) { number = 10; } int number = 3; set(number);

方法里的number和外面那个number虽然同名,但完全是两个独立的东西。形参只是把实参的值接过来,后续的修改只影响形参自己。

2.2 按值传递和按引用传递,到底差在哪

按值传递的意思是,方法调用时把实参的值复制一份给形参,方法内部对形参的任何修改,都不会影响实参本身。

按引用传递的意思是,方法调用时把实参的引用(也就是实参本身的位置信息)直接传给形参,形参和实参指向同一块内存地址。方法内部修改形参所指向的内容,实参能看到;如果让形参重新指向另一个对象,实参的引用也会跟着变。

这是两者的核心区别。注意第二句话后面的半句——如果按引用传递,形参重新赋值一个新的对象,实参也会指向新对象。这一点特别重要,因为很多面试题考的就是这个细节。后面我会用代码证明,Java 在这一点上表现得完全不是引用传递,无论你怎么让形参重新指向新对象,实参都无动于衷。

2.3 官方定义早就说明白了

其实 Java 官方文档里对这件事有非常明确的描述,原话大意是:Java 编程语言总是使用按值传递。也就是说,方法得到的是所有参数值的一个副本,方法不能修改任何参数变量的内容。

很多老开发者心里那个“Java 对象是按引用传递”的说法,实际上是把“引用类型的值”和“按引用传递”混为一谈了。对象在堆内存里,栈上的变量存的是对象的引用,这个引用本身是个数值,传参的时候拷贝的就是这个引用值。拷贝之后,方法内外有两个不同的变量,但它们拿着同一个对象的地址。

所以说严格意义上来讲,Java 根本没有引用传递。只要你在方法内部尝试让形参指向一个新的对象,实参依然指着原来的对象,这就已经足够证明它不是引用传递了。

3. 底层原理:从 JVM 方法调用看参数传递的本质

3.1 每个方法调用都有一块自己的小天地

JVM 在执行 Java 方法时,会为每个方法调用创建一个栈帧,放在虚拟机栈里。一个栈帧里主要包含局部变量表、操作数栈、动态链接、方法出口等信息。

局部变量表是存局部变量的地方,以变量槽为单位。对于基本类型,槽里直接存数值;对于引用类型,槽里存的是指向堆中对象的引用值。方法参数也是局部变量,它们同样存放在局部变量表里。

调用一个方法时,实参的值会被复制到新栈帧的局部变量表里,也就是给形参分配槽位并赋初值。这个复制动作发生在栈帧创建的时刻,它是理解整个值传递机制的关键。

画个简单场景:调用setAge(int a)时,虚拟机把实参age的值拷贝到新栈帧的局部变量槽中。此后方法内的操作对象是那个槽,而不是原来那个age变量。外面age变量在自己的栈帧里活得很好,方法结束后它不会有任何变化。

3.2 引用的拷贝,其实拷的是一张地址纸条

对于对象类型,过程类似。假设有一个User user变量,它本身活在某个栈帧里,里面存的是一个指向堆中User对象的引用值,相当于一张写着对象地址的纸条。当我们把它作为实参传给方法时,新栈帧中会新增一个形参变量,它也拿到了一张纸条,上面写着同一个地址。

这里的关键是:两张纸条,同一个地址。方法内通过形参的纸条能找到那个对象,于是可以对对象的字段进行修改。但如果你试图把形参这张纸条改成别的地址,比如指向一个新对象,那只是把形参的纸条换了,外部实参的纸条纹丝不动。

这就是为什么 Java 传对象时,改了对象内容外面能看到,但让形参重新 new 一个对象外面看不到。这个现象和引用传递的典型表现完全不同,因为真正的引用传递里,当形参换了一张新纸条,外部实参也跟着指向新纸条。Java 做不到这一点,因为传给方法的是地址值的副本,而不是对这个变量的“引用引用”。

3.3 为什么说“对象也是值传递”并不矛盾

很多人在这一点上卡壳,就是因为脑子里把“对象的引用”等同于“引用传递”。实际上,引用类型的变量存的是一个数值——一个可以指向堆对象的地址值。既然是数值,传递时就遵循值传递规则,拷贝的是地址值本身。

这和 C 语言里的指针类似。C 的指针可以作为值传给函数,这时函数内部拿到的是指针的副本,修改指针本身不影响外部指针,但通过指针副本可以修改所指向的数据。Java 的引用变量做的差不多就是这件事,只是不像 C 那样允许你直接运算地址。

所以更准确的说法是:Java 中所有参数传递都是复制。基本类型复制的是数值,引用类型复制的是引用值,也就是地址。方法内部能通过这张复制的地址纸条访问原对象,但不能让调用方手中的纸条被替换掉。这也就是面试官希望你最终能亲口说出来的完整结论。

4. 用代码证明:四个经典实验

理论说再多,不如亲手做几个实验。我建议你把下面的代码原样复制到本地跑一遍,每一个实验结果都会验证前面的理论。

4.1 实验一:基本类型交换,为什么永远换不过来

经典考题就是 swap 两个整数。代码如下:

public class ValueTransferDemo { public static void swap(int a, int b) { int temp = a; a = b; b = temp; System.out.println("swap方法内: a=" + a + ", b=" + b); } public static void main(String[] args) { int x = 1; int y = 2; swap(x, y); System.out.println("main方法内: x=" + x + ", y=" + y); } }

输出结果:

swap方法内: a=2, b=1 main方法内: x=1, y=2

方法内部确实交换了形参 a 和 b,但 main 里的 x 和 y 完全不受影响。原因很简单:swap 里交换的是两个拷贝出来的新变量,原变量的存储位置和值都没被动过。这就是值传递最直观的体现。

一个老生长谈的延伸题是:怎么才能真正交换两个 int?答案是不能靠方法参数做,只能把交换结果封装到数组或对象返回,或者在调用方自己交换。面试时如果遇到这种题,直接说明“方法内交换不了实参”反而是得分点。

4.2 实验二:交换两个对象引用,结果说明了什么

面试题里的升级版是把 swap 应用到对象上,比如交换两个Person引用:

public class ValueTransferDemo2 { public static class Person { String name; Person(String name) { this.name = name; } } public static void swap(Person p1, Person p2) { Person temp = p1; p1 = p2; p2 = temp; System.out.println("swap方法内: p1=" + p1.name + ", p2=" + p2.name); } public static void main(String[] args) { Person personA = new Person("A"); Person personB = new Person("B"); swap(personA, personB); System.out.println("main方法内: personA=" + personA.name + ", personB=" + personB.name); } }

输出结果:

swap方法内: p1=B, p2=A main方法内: personA=A, personB=B

实验证明,就算传递对象引用,方法内部交换形参两边的引用,外部实参依然指向原来的对象。这就能让“对象也是值传递”这个说法落到实地:形参拷贝来的引用值被交换了,但拷贝影响不了原件。如果是真·引用传递,这里的 main 方法里 personA 和 personB 应该也跟着互换。

4.3 实验三:修改对象里的字段,为什么又能生效

再看一个常见例子:

public class ValueTransferDemo3 { public static class Person { String name; Person(String name) { this.name = name; } } public static void rename(Person p) { p.name = "新的名字"; } public static void main(String[] args) { Person person = new Person("旧名字"); rename(person); System.out.println(person.name); // 输出: 新的名字 } }

输出结果是“新的名字”。原因就是前面说的,方法内形参拿到的是和实参相同地址的纸条,通过这张纸条能找到同一个对象,所以对对象内容的修改对方可见。

把实验二和实验三放一起看,就能很完整地讲清楚 Java 的传递机制:对象的内容可以被副作用修改,因为指针副本指向同一个对象;对象引用的绑定关系无法被方法改变,因为绑定关系存在于调用方的栈帧里,方法碰不到。这就是值传递,不是引用传递。

4.4 实验四:String 和包装类为什么“改了没反应”

很多开发者在实际开发中踩过一个坑:把 String 传入方法,试图在里面改值,结果外面不变。案例写出来是这样:

public class ValueTransferDemo4 { public static void changeString(String s) { s = s.replace("abc", "xyz"); } public static void changeInteger(Integer num) { num = num + 1; } public static void main(String[] args) { String str = "abc"; changeString(str); System.out.println(str); // 输出: abc Integer count = 10; changeInteger(count); System.out.println(count); // 输出: 10 } }

String 和 Integer 都是不可变类型。方法内部对形参进行重新赋值时,实际上是创建了一个新对象,然后把形参的纸条换成了指向新对象的纸条。这个操作既不修改原对象内容(原对象本来也改不了),也不影响外部实参的纸条方向,因此看起来就像“传进去什么都没发生”。

很多人把这个现象错误归因为“String 比较特殊”,其实本质还是值传递的普通案例。Integer 里的num = num + 1底层是先拆箱、再计算、再装箱,得到一个新的 Integer 对象,形参指向新对象,外部引用自然无感。这一点如果能在面试里主动说出来,会比单纯背结论惊艳不少。

5. 面试追问套路的应对策略

5.1 第一层:先给一个完整的定性回答

如果面试官问“Java 是值传递还是引用传递”,最好的第一句话是:Java 中只有值传递。然后立刻补充一句:基本类型传的是数值副本,引用类型传的是引用值的副本,也就是地址的副本。方法内对形参的重新赋值不会影响调用方的变量,但通过形参修改对象内部状态可以影响调用方看到的内容。

这个回答把结论、分类、现象全部覆盖,面试官基本不用再追问“那对象呢”。最怕的就是只回答一句“值传递”,然后干等面试官追问。主动把后半段解释出来,能把自己从“背题人”提升到“理解者”的位置。

5.2 第二层:用画图的方式把栈和堆的关系讲清

如果条件允许,面试时拿笔在纸上画一下,或者口头描述这个场景。把 main 方法的栈帧和当前方法的栈帧都画出来,然后把堆里的对象画成一个矩形,引用画成一条带箭头的线。

画完之后,上面那些实验几乎不用写代码就能解释清楚。形参和实参是不同栈帧里的两个变量,都存着同一个对象地址值。修改对象内容时,两条线都指向同一个矩形,所以见效;修改形参的指向时,只是重新画一条从形参出发到新对象的线,main 里那条线没动。

这类画图表述在面试里特别加分。能让面试官确认你是真的理解内存模型,而不是背了一堆结论。这也是我建议所有准备 Java 面试的人提前练习的一个技能,不只是为了这道题,后面讲 JVM、讲并发也都会用到。

5.3 第三层:结合局部变量表的原理深挖一层

再往深挖,可以把局部变量表这个概念带进来。每个方法调用都会在虚拟机栈中分配一个栈帧,栈帧里有局部变量表。实参的值就是在栈帧创建时被拷贝到形参对应的变量槽里的。基本类型的值是真实数值,引用类型的值是对象引用。

这里有个细节可以顺带提一下:局部变量表中的槽位是可以复用的,而且并非所有局部变量都初始化为默认值,只有经过赋值的变量才能被读取。虽然这不直接影响参数传递的主线,但提到栈帧和局部变量表,会把整个答案拉高一个档次,因为说明你读过 JVM 相关书籍并且理解栈帧结构。

5.4 第四层:和 C++ 引用传递做对比

有时候面试官会问“你怎么证明它不是引用传递”,或者“其他语言有引用传递,区别是什么”。这时候可以用 C++ 的引用作为对照。C++ 中函数参数使用&声明为引用时,形参是实参的别名,对形参做任何绑定修改,实参都会跟着变。比如 C++ 的 swap 用引用可以实现真正交换两个变量。

Java 做不到这一点。Java 的引用变量本身也是值,没有提供“引用的引用”这种机制。你只能传一个引用副本,方法能使用这个副本访问对象,但不能接管调用方手里的引用变量。这样对比着讲,区分度特别高。

5.5 第五层:出题变体——数组、集合和返回值

面试官还喜欢把题目包装成数组或集合。比如:

public static void clearList(List<String> list) { list = new ArrayList<>(); } public static void addToMap(Map<String, String> map) { map.put("k", "v"); }

第一个方法里让形参 list 指向新集合,外部集合不变;第二个方法通过形参 map 往原对象里放数据,外部能看到。这就是同一个机制的两种表现:改引用不生效,改内容生效。数组也同理,形参重新指向新数组不生效,通过形参修改数组元素生效。把这些变体全部纳入自己的解释框架,就能做到“万变不离其宗”。

6. 容易踩的坑和自查方法

6.1 “对象引用传递”这个误区的来源

这个误区之所以流传广泛,我总结有两个原因。第一个原因是术语混淆,把“对象的引用”和“引用传递”中的引用当成一回事。第二个原因是经验现象误导:开发中经常看到传入对象、修改对象字段外部也变了,就直接得出结论“这是引用传递”。这个现象恰好发生在修改对象内容这个分支,于是很多人只记住了这一个分支,忘了重新赋值这个检验动作。

只要在脑海里把“修改形参引用本身”这个分支补齐,误区自然消除。我建议每个人手写一遍实验二和实验三的代码,再对照输出理解,比看十篇博客都有效。

6.2 怎么设计验证代码来区分两种传递

想验证一种语言到底是值传递还是引用传递,最核心的测试方法只有一个:在方法内部让形参重新指向一个新的对象,然后观察外部实参是否也跟着指向新对象。如果跟着变了,就是引用传递;如果没有跟着变,就是值传递。

把这个测试方法记下来,到任何语言里都能快速自测。甚至可以用来测试其他语言,比如 C 的指针传递、Python 的对象传递,思路是通用的。面试的时候如果遇到改变题型,比如问“这个方法执行后外部是什么结果”,你直接用这套测试逻辑推理,通常能答对。

6.3 实际开发中的影响

这个知识点不只是面试题。工作中遇到“传对象进方法,怎么没生效”的问题时,先检查是不是在方法内部重新赋值了形参。比如有人想写一个从数据库拉取数据并填充对象的工具方法,结果在方法内部执行user = new User(),然后往这个新对象里塞数据,调用方手里的 user 自然还是空对象。正确做法是在方法内部通过形参修改传入对象的字段,不要重新创建对象给形参。

另一个常见场景是集合工具类。编写一个方法给 List 批量添加数据时,如果方法内部不小心list = new ArrayList<>(sourceList),调用方的原 List 不会被填充。这种 bug 排查起来特别隐蔽,因为代码看起来逻辑没毛病,实际上是把引用赋值当成了内容修改。

6.4 给基础薄弱同学的学习路线

如果你现在还不太能理解栈和堆的关系,我建议先按这个顺序补课:先学会区分对象和引用,一句话就是“引用是保存对象地址的变量,对象是堆里的一块数据”。第二步是画内存图,把变量、引用、对象的关系画出来。第三步是自己写实验代码,把上面四个案例跑一遍并预测输出结果。最后一步才是去背各种结论。

这个顺序能帮你在没搞懂底层的情况下快速建立正确模型。不要一上来就背“Java 只有值传递”这句话,因为你不知道原因,面试官一追问就容易露馅。真正理解了内存模型之后,你会发现这道题其实非常简单,只是概念的澄清过程需要花时间。

最后再分享一个我常用的记忆技巧:把引用类型变量想成文件夹的快捷方式。方法传参时,你给出去的是一个快捷方式的副本。对方通过这个快捷方式可以打开文件夹里的文件,修改文件内容你会看到变化;但对方如果重新创建一个快捷方式指向别的地方,你手里的快捷方式不会变。Java 的传递机制,本质上就是这个模型。希望这篇文章能帮你把这个知识点彻底吃透,下次面试再遇到,你能从容不迫地把它讲透。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询