读完这篇,你能做什么
能预测方法修改对象和替换形参后的调用方结果。
学习理由、准备要求与知识点
为什么学它
理解按值传递,以及多个变量引用同一对象(别名)的情况,才能判断修改 DTO 或共享集合时会影响哪些调用者。
先读这些内容
知识点清单
- 基本值与引用值
- 对象与引用变量
null引用- 引用别名
- 方法按值传递
- 修改对象与重绑形参
String不可变性- 显式输入输出设计
运行前先确认Java 17;源码在本机 Microsoft OpenJDK 17.0.20.1 src.zip 核验,在线链接为上游 jdk-17.0.20-ga。深入模块可在掌握基础后回读。
01先弄懂它是什么
想知道“改这里,那里会不会变”,先把变量和对象分开看。int n = 3 直接保存整数值;Box box = new Box(3) 中,box 保存的是指向 Box 对象的引用,对象里才有字段,也就是对象保存的数据。写 Box other = box 只复制这个引用,两个变量指向同一对象。所以通过任一个变量修改对象字段,另一个变量也可能看到变化。
方法接收的也是值的副本,这叫按值传递。把 user 传给方法后,方法里的 u 是另一个变量:u.name 改的是共享对象的内容;u = new User() 只是让 u 指向新对象,不会让外面的 user 跟着换。给 int 形参加一同样不会改外部整数变量。能通过引用改对象,并不表示 Java 按引用传递。
null 是引用类型可以保存的特殊值,表示这里没有指向对象,声明变量不等于创建对象。调用 null 的实例成员会失败;在接收输入或交接数据时尽早检查,比在多层调用后意外失败更容易定位。对象已无法再被程序访问,也就是不再可达时,才可能由垃圾回收机制清理。不能简单说某个局部变量离开大括号,对象就立刻销毁;其他引用仍可能让程序访问到它。
文中这几个词,先认一认
- 别名
- 多个引用指向同一个对象,通过其中一个修改对象时,其他引用也可能看到变化。
- 重新绑定
- 给引用变量重新赋值,让这个变量指向另一个对象或变成
null。 - 对象身份
- 是否为同一个具体对象,区别于两个不同对象的内容是否相同。
- 所有权
- 设计时约定谁能修改或保存数据、谁负责关闭资源,避免各方互相影响。
02看看它怎么工作
观察一次方法调用可以分四步:先对实参表达式求值,再把这些值交给形参变量,随后执行方法体,最后把返回值交给调用者。返回一个新对象并由调用者显式赋回变量,才能清楚表达替换结果;依靠方法内部重新绑定形参无法实现普通的外部变量交换。
复制也分几种情况。只复制引用,两个变量仍指向同一个对象;复制容器,会得到新的存放位置,但里面的元素对象仍可能共用;再复制元素,才有机会把它们的修改分开。如果原来的两个位置指向同一个对象,而复制时每个位置都单独 new 一个对象,就改变了原有的共享关系。做深复制前,要先决定哪些对象仍应共用、相互引用成环时怎么办,以及不可变对象和外部资源如何处理,不能只说“复制所有字段”。
对象创建后不能再变,使用它的人就少了一件要担心的事:别人会不会把它改掉。但不可修改容器不等于里面的元素也不能改。final 引用只是不允许给这个变量重新赋值,对象本身仍可能变化;多个线程一起使用时,还要检查修改能否被看见,以及多步操作会不会互相干扰。图中的两条变量箭头表示它们共用一个对象;虚线指向新对象,表示只改变了方法里的形参,调用者的变量仍指向原对象。
正在加载流程图…
查看流程图源码(Mermaid)
flowchart TD
A["调用者变量 box"] --> O["原 Box 对象"]
B["形参 local:引用值副本"] --> O
B --> M["修改字段:两方观察同一对象"]
B -. "local 重新赋值" .-> N["新 Box 对象"]
A --> K["调用者引用仍指向原对象"]按顺序看,每一步做了什么
- 列出所有变量及其保存的值。
- 为每个 new 画独立对象。
- 用箭头标出共享引用。
- 区分字段赋值与变量重新绑定。
- 为复制接口说明隔离深度与别名保留规则。
03对照实现代码
Microsoft OpenJDK 17.0.20.1;java.base/java/util/Objects.java · java.util.Objects#requireNonNull(T)
以下为原创 Java 17 教学实现,不是原样 JDK 源码;只保留文中明确讨论的机制,省略优化、兼容及内部接口。
public final class SourceModel {
public static <T> T checked(T reference) {
// 验证只改变控制流,不创建或复制目标对象。
if (reference == null) throw new NullPointerException("reference");
return reference;
}
}真实 Objects.requireNonNull 的普通重载检查引用是否为 null,合法时直接返回传入引用。泛型返回类型让调用者可以把校验写在赋值或构造器初始化处,而不丢失静态类型。教学模型增加一条固定诊断信息,展示同一机制,不复制复杂错误消息重载的实现。
返回原引用意味着身份保持:checked(box) == box 对非空输入成立。该方法没有承诺对象内容有效,也不会使可变对象成为不可变对象。把名字中的 require 理解为全部业务校验会产生误判;例如用户对象非空,邮箱仍可能缺失,金额字段仍可能越界。
非空检查能把错误尽早定位,但不保证所有后续读取都永远非空。局部变量检查后没有重新赋值,较容易维持这个前提;共享字段却可能被其他线程改动,再从字段读一次是另一回事。可以先取到局部变量,再围绕这同一个引用检查和操作。
这个很短的方法适合作为源码阅读的起点:看清输入、分支、返回与对象分配即可解释它的完整核心语义。不要因为返回值类型与输入相同,就猜测它执行了转换或克隆;反过来,真正的复制方法必须能够指出新对象在哪里创建,以及哪些字段仍沿用旧引用。
代码先调用谁,再调用谁
- 调用者计算引用值
requireNonNull判断null- 合法分支原样返回引用值
- 调用者选择保存或继续使用该引用
04动手写一个例子
复制对象列表,同时保留列表内部的别名关系
输入 List<Box>,Box 只有一个可变 int 字段。返回新列表和新 Box;输入中同一 Box 的重复位置在输出中仍共享同一个新 Box,不同 Box 即使值相等也不能合并。允许 null 元素,拒绝 null 列表。
- 输入 1
[A(3),A(3)]- 输出 1
[A'(3),A'(3)],A' 与 A 不同实例
- 输入 2
[A(3),B(3)]- 输出 2
两个不同的新实例
- 输入 3
[]- 输出 3
[]
普通逐位置复制会把一个共享对象复制成多个不同对象,改变别名结构。需要建立旧对象到新对象的映射:首次遇见旧对象时创建副本并记录,之后遇见同一旧对象就复用已创建副本。映射必须按身份判断键,不能依赖 Box 将来可能重写的值相等逻辑。
IdentityHashMap 采用引用身份作为键比较语义,正好表达这里的旧实例映射。null 元素直接保留 null,因为没有对象需要复制;输入列表本身 null 则属于参数错误。输出列表独立分配,副本字段读取自原对象,但本题只有 int 字段,所以没有更深一层可变引用需要处理。
测试要同时验证值、隔离和关系。只比较所有字段值不足以证明复制正确:输出两个位置可能意外拆散共享,也可能错误合并值相同的不同对象。修改输出副本后原对象应保持不变,而输出中的别名位置应共同看到变化,这两个观察一起验证目标契约。
为什么这样写是对的每处理一个位置,映射为已见每个旧 Box 身份保存且只保存一个新 Box,输出前缀保持相同的别名关系。
每个输入位置至多执行一次身份表查询、一次创建与一次追加;每个不同旧对象只复制一次,散列表常见期望成本作为时间前提。
保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。
展开收起代码 · Main.java · 完整可运行程序
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
import java.util.ArrayList;
import java.util.IdentityHashMap;
import java.util.List;
public class Main {
static final class Box {
int value;
Box(int value) { this.value = value; }
}
static List<Box> copy(List<Box> input) {
if (input == null) throw new IllegalArgumentException("input");
IdentityHashMap<Box,Box> mapping = new IdentityHashMap<>();
List<Box> output = new ArrayList<>(input.size());
for (Box old : input) {
// 空槽位没有目标对象需要复制,保持它的位置。
if (old == null) { output.add(null); continue; }
// 按旧对象身份复用副本,保留重复引用关系。
Box fresh = mapping.get(old);
if (fresh == null) {
fresh = new Box(old.value);
mapping.put(old, fresh);
}
output.add(fresh);
}
return output;
}
static void require(boolean condition, String message) {
if (!condition) throw new AssertionError(message);
}
public static void main(String[] args) {
require(copy(List.of()).isEmpty(), "empty");
Box a = new Box(3), b = new Box(3);
List<Box> repeated = copy(List.of(a,a));
require(repeated.get(0) == repeated.get(1), "preserve alias");
require(repeated.get(0) != a, "new identity");
List<Box> distinct = copy(List.of(a,b));
require(distinct.get(0) != distinct.get(1), "do not merge equal values");
// 修改副本同时检验隔离与副本内部的共享。
repeated.get(0).value = 9;
require(a.value == 3 && repeated.get(1).value == 9, "isolation");
List<Box> withNull = new ArrayList<>(); withNull.add(null);
require(copy(withNull).get(0) == null, "null element");
boolean rejected = false;
try { copy(null); } catch (IllegalArgumentException e) { rejected = true; }
require(rejected, "null list");
System.out.println("PASS 7");
}
}这些特殊情况也要试一试
- 空列表返回独立空列表。
null元素保留,null列表拒绝。- 值相等的不同对象不合并。
- 重复引用只创建一份副本。
进阶选读 · 可选深入:含环对象图复制
场景对象字段继续引用其它对象,可能形成环或共享子图。
做法先创建并登记空壳副本,再遍历引用边填充字段;用工作队列避免无界递归,并为不可复制资源制定策略。
复杂度对 V 个节点 E 条引用边,期望 O(V+E) 时间与空间。
好处与代价复制不自动获得并发一致快照,文件句柄与连接等资源也不能按普通字段复制。
05项目里怎么用
后端方法收到 DTO 后直接改字段,会让调用者观察到副作用;这可能是约定的更新,也可能是意外污染。接口应通过名字、返回值和文档说明是否修改输入。需要转换时返回新的结果对象,让调用者决定如何替换,更容易测试和复用,但要评估额外分配。
缓存中的可变对象尤其需要明确所有权。若返回内部对象引用,某个请求的修改可能传播到其它请求;只包装不可修改列表不能防止元素字段变化。可以存储不可变值、在边界复制,或把修改操作集中到拥有同步策略的组件,具体选择由访问频率和一致性需求决定。
深复制不自动等于一致快照。复制过程中别人若正在修改,可能把多个不同时刻的字段拼在一起,而这样的整体状态从未真实存在。需要一致结果时,要采用不可变整体替换、合适的锁或版本检查。复制还要考虑资源、引用环和共用对象,不能把序列化再读回来当成万能办法。
容易踩的坑
- 把 Java 称为按引用传递。
- 认为
final或非空检查会冻结对象内容。 - 逐位置深复制意外破坏共享关系。
- 把没有同步的复制结果当成一致快照。
几种做法,怎么选
表格有四列,窄屏可左右滑动查看代价和适用条件。
| 方案 | 优势 | 代价与限制 | 适用条件 |
|---|---|---|---|
| 共享可变引用 | 少分配且可表达共同状态 | 副作用传播与并发管理复杂 | 明确所有权的内部状态 |
| 不可变值替换 | 状态变化易追踪且便于共享 | 更新时需创建新值 | 配置、标识与读多数据 |
| 边界复制 | 隔离调用者修改 | 复制深度与快照一致性需要设计 | 公开接口与缓存边界 |
06检验是否读懂了
先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。
先用自己的话解释
01把形参设为 null 会改变外部变量吗?
展开参考答案与推导
不会。形参与调用者变量分别保存引用值,赋 null 只改变形参。通过原引用修改对象字段则作用于共享对象,所以应按赋值目标区分这两类行为。
02非空检查为什么不等于对象有效?
展开参考答案与推导
它只证明这一次引用值不是 null,并未检查内部字段、业务范围或并发状态。对象可能存在但处于不合法业务状态,应按对象职责执行进一步验证。
03值相等与身份相同有什么区别?
展开参考答案与推导
身份回答是不是同一个实例,值相等由类型定义内容规则。本题复制拓扑需要保留实例关系,所以使用身份映射,不能因为两个 Box 的数值相同就合并。
换个情况,还会处理吗
01Service 改 DTO 后控制器也看见变化,原因是什么?
展开参考答案与推导
两层通过不同引用变量访问同一个 DTO 对象,字段修改作用于共享实例。若接口不应修改输入,返回新结果并在边界明确转换,而不是把方法参数改成 final 期待隔离。
02复制缓存列表后元素仍被串改,哪里没有复制?
展开参考答案与推导
很可能只复制了列表容器,元素引用仍指向缓存中的可变对象。检查每一层新对象的创建位置,决定采用不可变元素还是逐元素复制,并明确并发快照要求。
03想在方法里交换调用者两个引用,普通参数够吗?
展开参考答案与推导
普通参数重新赋值只交换局部副本。可以返回包含新顺序的结果,由调用者显式赋回,或修改调用者传入的容器槽位;后一种方式必须明确副作用。
写代码试一试
01复制后验证输入列表也未被修改
- 输入 / 条件
[A]- 输出 / 预期
输入长度仍为 1
提示 向输出列表追加元素。
展开参考答案与推导
调用 copy 后向返回列表追加一个新 Box,再断言输入大小仍为一。它验证外层容器隔离,与修改 Box 字段验证元素隔离是两个独立维度。
02让 Box 增加可变 List 字段
- 输入 / 条件
Box.items=[1,2]- 输出 / 预期
副本列表独立
提示 先规定元素是否可变。
展开参考答案与推导
在复制新 Box 时额外创建新的列表容器。如果列表元素也是可变对象,还需决定共享或递归复制;不能仅新增字段赋值就继续声称整对象已经完全隔离。
03验证别名位置并非必须相邻
- 输入 / 条件
[A,B,A]- 输出 / 预期
输出第 0 与第 2 项身份相同
提示 身份映射持续到整个调用结束。
展开参考答案与推导
加入三项输入并比较输出第零与第二项的 ==,同时确认第一项不同。映射若只记上一对象会在不相邻重复时失败,因此该用例检查映射的作用范围。
读完之后,独立完成
课后作业:自己做一遍
本篇共 10 道作业,先完成 8 道基础与综合任务,再按前置知识挑战进阶题。其中包含 1 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。
能预测方法修改对象和替换形参后的调用方结果。
本篇重点加练
重点检查方法调用中的值传递与输入输出合同,避免调用者误判副作用和返回值。
- 方法按值传递
- 显式输入输出设计
重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。
先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。
基础必做
- 作业 01代码推演
设 Box 只有
intvalue 字段:a=newBox(7),b=a,c=newBox(7),随后b.value=9、a=null。逐行画出变量槽位和对象关系,说明每个可执行字段读取的值以及哪些读取需要拒绝;先推演再运行。- 提交内容
- 各步骤对象图、预测表和实际运行记录。
- 怎样算完成
- 区分变量、引用值与对象身份
- 逐步覆盖
null和别名变化
练到的知识点基本值与引用值 · 对象与引用变量 · null 引用 · 引用别名
- 作业 02原理问答
设计两个同名但职责不同的方法草案:一个修改传入 Box 的字段,一个试图替换调用方 Box。说明各自输入输出合同,并用调用前后变量图审查草案是否兑现合同。
- 提交内容
- 两份方法合同与参数传递图。
- 怎样算完成
- 区分形参与调用方变量
- 合同能从调用结果验证
练到的知识点方法按值传递 · 修改对象与重绑形参
- 作业 03代码推演
方法
receive(int n, Box b)先执行 n+=2,再修改b.value,最后返回 n;调用表达式又把返回值赋给另一个变量。自定具体初值,逐个列出调用前后所有变量的来源,禁止只展示最终打印值。- 提交内容
- 带实参求值、形参建立及返回赋值的时间线。
- 怎样算完成
- 每个变量均对应声明位置
- 输入样本能分辨形参和返回值
练到的知识点方法按值传递
综合应用
- 作业 04设计评审
昵称规范化函数接收
String,产品要求保留原始昵称供审计、返回规范化昵称供展示。评审“方法返回void并在内部反复给形参赋值”的提案,重新定义接口与调用方式。- 提交内容
- 接口签名、调用示例及原始值保留说明。
- 怎样算完成
- 解释
String操作结果的去向 - 输入与输出职责明确
- 解释
练到的知识点String 不可变性 · 显式输入输出设计
- 作业 05编程实践
编写仅使用基础语法的
swapFields(Box left, Box right),交换两个对象的 value;自行定义null、同一对象和相同数值的处理合同。不要把交换对象字段与交换调用方变量混为一项需求。- 提交内容
- 完整 Java 文件、自写正常及边界测试、编译运行证据。
- 怎样算完成
- 测试验证合同而非只打印
- 覆盖两个参数指向同一实例
- 能说明函数的副作用范围
练到的知识点基本值与引用值 · 对象与引用变量 · 引用别名 · 方法按值传递 · 修改对象与重绑形参
- 作业 06面试场景
一个个人记账程序把同一笔 Record 同时放入“本月记录”和“待审核记录”,编辑前者后审核页面随之改变。判断这是否违反需求需要询问哪些事实,并给出两个不同业务合同下的接口设计。
- 提交内容
- 事实清单、关系图和两份接口提案。
- 怎样算完成
- 先确定共享是否为业务要求
- 说明每种设计的修改传播范围
练到的知识点引用别名 · 显式输入输出设计
- 作业 07编程实践
实现两种扣减积分接口:一种返回新的 Balance,另一种按合同修改传入 Balance;用同一组输入比较调用方必须执行的步骤,并明确余额不足时的处理。
- 提交内容
- Java 代码、自写正常/不足/零扣减测试与运行证据。
- 怎样算完成
- 调用方式符合各自合同
- 失败时原对象状态有检查
练到的知识点方法按值传递 · 显式输入输出设计
- 作业 08设计评审
转换方法同时修改输入对象并返回另一个对象,接口文档只写“更新完成”。为调用方、日志记录者和后续处理者分别列出歧义,再制定能独立验收的合同。
- 提交内容
- 三个使用场景和重写后的方法合同。
- 怎样算完成
- 副作用与返回值身份均明确
- 每项合同有可观察判据
练到的知识点显式输入输出设计
进阶挑战 · 掌握前置后选做
- 作业 09设计评审
完成集合基础后:评审一个含父子关系与共享标签的对象图导出需求,规定哪些身份关系必须保留、哪些对象不得复制;提出能够检测实现是否满足这些约束的验收案例。
- 提交内容
- 复制边界文档与至少五个关系验收案例。
- 怎样算完成
- 明确共享与环的合同
- 不把字段值相同等同于身份相同
练到的知识点引用别名 · 显式输入输出设计
思维拓展
先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。
- 作业 10思维拓展 · 条件变化
把积分接口的新需求改成“允许调用者忽略返回值,但任何已保存的旧对象都必须保持原样”。分析这些约束能否与原调用方式同时满足;可修改接口,但要说明调用方兼容性。
- 提交内容
- 约束表、可行性论证与调用示例。
- 怎样算完成
- 论证涉及对象身份和数据变化
- 可行方案或冲突均有具体证据
练到的知识点方法按值传递 · 显式输入输出设计
完成后,再检验一次
给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。
继续阅读
先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。
- 必读规范英文
Java 17 语言规范:类型、值与变量 (新标签页)↗
核对基本类型范围、引用类型、类型变量与可具体化类型,理解编译期约束。
- 实践规范英文
Java 17 语言规范:类、方法与记录类 (新标签页)↗
按需阅读构造器、方法重载、static
/final、枚举和 record 的定义与约束。 - 深入规范英文
Java 17 语言规范:转换与上下文 (新标签页)↗
查阅数值提升、强制转换、装箱拆箱以及方法调用时的转换规则。
- 实践实践指南英文
Java 代码可视化:逐行看变量、引用和方法调用 (新标签页)↗
先写两个变量指向同一个数组,再通过其中一个变量修改数组。运行前画出你预计的关系,运行后逐步核对;再练一次传参和一次递归。只使用可视化也能完成这些练习。 可视化功能免费,页面为英文且有广告。只适合短小示例,不支持完整的多文件项目;页面带 AI 问答功能,输入代码会交给外部服务处理,练习请用不含私密信息的小例子。
- 深入参考文档英文
OpenJDK 17u java.util.Objects (新标签页)↗
定位
requireNonNull普通泛型重载,核对合法路径直接返回原引用。 - 深入参考文档英文
Oracle 方法参数教程 (新标签页)↗
旧版教程用于稳定的传值概念;代码仍按 Java17 核验。
- 深入参考文档英文
Java17 Objects API (新标签页)↗
区分非空校验、相等辅助与深度比较。
- 深入参考文档英文
Java17 IdentityHashMap API (新标签页)↗
阅读身份键语义及对象图转换的使用场景。