备忘录模式中快照应该深拷贝还是浅拷贝?
简化版
备忘录快照通常需要保证恢复时状态不被后续修改污染。如果状态包含可变对象,浅拷贝可能导致历史快照被当前对象改动影响;深拷贝更安全但成本更高。实际选择要看状态是否可变、对象大小和性能要求。
详细版
浅拷贝:
- 只复制引用。
- 成本低。
- 可变引用会共享,历史快照可能被污染。
深拷贝:
- 复制完整对象图。
- 快照独立性强。
- 成本高,复杂对象处理困难。
判断方式:
- 状态都是不可变值:浅拷贝通常可行。
- 状态包含可变集合、对象:需要深拷贝或不可变结构。
- 状态很大:考虑增量快照、结构共享、操作日志。
快照正确性比代码简短更重要。
完整版教学
一、浅拷贝的问题
假设备忘录保存的是一个 List 引用。如果 Originator 后续继续修改这个 List,备忘录里的 List 也跟着变了。
等到恢复时,你以为拿到的是历史状态,其实已经被当前操作污染。这会让撤销功能看起来随机失效。
二、深拷贝的代价
深拷贝能保证快照独立,但它要复制整个对象图。对象越大,成本越高;对象关系越复杂,越容易遇到循环引用、共享引用和不可复制资源。
例如文件句柄、数据库连接、线程对象就不适合作为快照直接深拷贝。
三、不可变对象和结构共享
如果状态用不可变对象表示,快照可以很轻。因为不可变对象不会被后续修改污染,多个快照可以安全共享未变化部分。
这也是很多编辑器、前端状态管理、持久化数据结构会强调不可变更新的原因。
四、常见误区与工程判断
不要机械回答“一定深拷贝”。如果状态很大、操作频繁,完整深拷贝可能把系统拖垮。
工程中可以分层处理:小状态深拷贝,大状态保存增量,复杂状态使用不可变数据结构或命令日志。关键是保证恢复语义正确。
五、快照正确性的核心是隔离变化
浅拷贝快照最大的风险是共享引用。如果 Originator 的状态里有列表、Map、复杂对象,Memento 只是保存引用,那么后续修改原对象时,历史快照也可能被一起改掉。这样等到撤销时,恢复出来的不是历史状态,而是被污染后的状态。
深拷贝能提升快照隔离性,但代价是性能和内存。对象图越大、层级越深、引用关系越复杂,深拷贝越昂贵,还可能遇到循环引用、不可复制资源、外部连接等问题。因此工程里常见折中:不可变对象直接共享,可变集合做防御性拷贝,大对象使用增量快照或差异记录。
面试时不要机械地说“深拷贝一定好”。更准确的回答是:快照必须保证恢复语义正确,至于是深拷贝、不可变结构、增量记录还是复制关键字段,要根据状态规模和变化频率选择。
面试官如果追问如何验证快照是否正确,可以给一个测试思路:先创建对象并保存快照,再修改原对象里的嵌套集合或子对象,然后执行恢复,检查恢复结果是否等于保存时状态。如果恢复结果受到后续修改影响,就说明快照隔离失败。
六、用工程约束检验答案
判断标准是可变引用是否跨时间共享。快照保存 List<Item> 时只复制 List,却仍共享 5 个可变 Item,修改任意 Item 都会污染历史;若 Item 是不可变值对象,复制容器或使用不可变列表即可满足隔离。
| 检查项 | 核心判断 | 工程含义 |
|---|---|---|
| 浅拷贝 | 复制顶层引用结构 | 共享可变子对象时不安全 |
| 深拷贝 | 复制整个可变状态闭包 | 空间与时间成本高 |
| 结构共享 | 共享不可变节点 | 兼顾隔离与空间 |
把关键关系压缩成一条可复述的路径:
列出状态对象图
标记每个节点是否可变
复制所有可能被修改的路径
用测试验证历史不随当前状态变化
答案不应简单站队深拷贝;快照要求的是时间隔离,深拷贝只是实现它的一种手段。
落地前可以再按下面 3 步复核:
- 先说明“浅拷贝”的核心机制:复制顶层引用结构;再交代边界:共享可变子对象时不安全。
- 接着分析“深拷贝”:复制整个可变状态闭包;不能遗漏对应代价或结果:空间与时间成本高。
- 最后用“结构共享”检查方案:共享不可变节点;验收时确认兼顾隔离与空间。
这三项构成完整判断链:先讲清浅拷贝,再说明深拷贝,最后用结构共享检验实现是否越界。
面试中若能给出违反“兼顾隔离与空间”的反例,再说明修正办法,答案就从模式定义落到了可验证的工程决策。
七、常见误区与追问
- 误区:只看到“浅拷贝”就认为方案成立。 必须同时说明核心机制“复制顶层引用结构”和工程边界“共享可变子对象时不安全”。
- 误区:把“深拷贝”当成无条件结论。 只有在“复制整个可变状态闭包”成立时,才能据此讨论“空间与时间成本高”。
- 追问:String 需要深拷贝吗? 通常不需要,因为 String 不可变,可安全共享。
- 追问:Collections.unmodifiableList 足够吗? 它只禁止通过该视图改列表,若底层列表或元素可变仍可能污染。
- 追问:序列化深拷贝推荐吗? 实现方便但性能、类型兼容和安全成本较高,需评估。
- 追问:循环引用怎么复制? 深拷贝需维护已复制对象映射,避免无限递归并保持共享关系。
- 追问:怎样证明快照独立? 保存后修改多层子对象,再恢复并断言历史值未变化。
八、加强记忆
记忆时抓住这条主线:可变引用浅拷贝会污染历史快照;深拷贝更安全但成本更高;不可变对象适合做轻量快照;快照策略要按状态规模和变化频率选择。面试回答先给出模式意图,再用调用链或数据流说明角色协作,最后主动交代适用边界与工程代价。