把一个订单 DTO 传给方法后,方法里修改字段会影响外面,但给参数重新赋值却没影响,为什么?
先说结论:Java 只有值传递。基本类型参数复制具体值;引用类型参数复制的是引用值,调用方和被调用方的两个引用副本可以指向同一对象,但方法内给参数重新赋值不会改变调用方变量。 因此方法可以通过引用副本修改同一可变对象的字段,却不能用 obj = new Object() 替换调用方持有的引用。
我先给结论,再说明它在项目里解决什么问题。用基本类型、对象引用和重新赋值解释 Java 参数传递语义。Java 只有值传递。基本类型参数复制具体值;引用类型参数复制的是引用值,调用方和被调用方的两个引用副本可以指向同一对象,但方法内给参数重新赋值不会改变调用方变量。 因此方法可以通过引用副本修改同一可变对象的字段,却不能用 obj = new Object() 替换调用方持有的引用。
核心机制我会按一次真实执行过程来讲。沿着「计算实参表达式 → 复制值到形参槽位 → 方法内部操作副本 → 可能修改共享对象 → 返回后形参消失」观察输入、状态与输出,这些阶段都可以从日志、指标或源码里验证。 计算实参表达式 调用前先计算实参;基本类型得到数值,对象表达式得到的是指向对象的引用值。 复制值到形参槽位 Java 总是把这个值复制给形参,因此基本数值和引用值本身都不会被方法反向替换。 方法内部操作副本 给形参重新赋值只改变局部槽位,不会让调用方变量指向新对象。
实现细节只抓关键入口,不会整段背源码。JVM Spec 2.6 Frames:局部变量表与操作数栈槽位。 javap astore/aload:观察引用值复制和形参重新赋值。 我会先确认请求实际走到了哪条路径,再用运行数据验证,不会只看类名或配置猜测。
放到生产使用时,我会关注参数和验证数据。在调用前后记录 System.identityHashCode 与字段值,分别验证 replace 和 mutate;并发场景再检查共享可变对象是否存在数据竞争。 固定输入和基线 先在没有故障注入的环境执行上述配置,固定数据规模、并发度、运行时版本和预热时间。以「调用方引用身份」为主基线,记录值应满足「replace 前后应相同」;同时保存 对象身份标识、方法前后字段差异,使后续变化能够回到同一时间轴比较。
最后补充常见误区和使用边界。代码试图通过 swap(User a, User b) 交换调用方变量,方法内打印正常,返回后引用却未变化。原因是交换的只是两个引用副本。若需求是调整列表顺序,应操作调用方传入的列表位置;若是替换对象,应返回包含两个新引用的结果。 把对象可变误判为引用传递:对象身份标识:需要替换时显式返回新对象。 方案:更适合的场景:主要收益:代价与边界。 返回新值:不可变对象或需要明确替换:副作用小、数据流清晰:调用方必须接收返回值。 修改可变对象:聚合内部状态有明确所有者:避免额外复制:别名共享会扩大副作用。 包装器/数组持有槽位:框架 API 必须模拟输出参数:可以原地写回:语义绕、通常不如结果对象清晰。 选型至少带上 对象创建率、调用频次、输入规模和 API 边界,并用上面的量化基线验证;未知数据应明确为待测假设。