
上面的例子里content.toString()恰好是安全的——String不可变快照存的字符串永远不会被后续修改影响。但这是个巧合一旦Originator的状态包含可变对象List、Map、数组、自定义可变类只做浅拷贝就会埋下 bug——快照和当前状态共享同一个引用恢复时恢复的是被改过的同一个对象。// ❌ 错误示范Memento 只存了 List 的引用 class Document { private ListString lines new ArrayList(); public void addLine(String line) { lines.add(line); } public Memento save() { return new Memento(lines); // 存的是引用不是副本 } public void restore(Memento m) { this.lines m.getLines(); } static class Memento { private final ListString lines; private Memento(ListString lines) { this.lines lines; } private ListString getLines() { return lines; } } } Document doc new Document(); doc.addLine(第一行); Memento snap doc.save(); // 想快照当前的 [第一行] doc.addLine(第二行); // 继续编辑 doc.restore(snap); // 期望恢复成 [第一行] // 实际结果[第一行, 第二行] —— 快照里的 List 就是当前这个 List早被改花了根因是快照和发起人指向同一个可变对象。正确做法是存取两端都做深拷贝——不光save()时要拷一份进Mementorestore()时也要再拷一份出来否则恢复后Originator又和Memento内部的List共享了引用(这个快照如果还留在栈里,比如 redo 场景可能被复用,后续修改就会穿透回去,快照就不快了)public Memento save() { return new Memento(new ArrayList(lines)); // 存:拷一份副本进快照 } public void restore(Memento m) { this.lines new ArrayList(m.getLines()); // 取:再拷一份出来,不共享引用 }只有两端都深拷贝才能保证Originator和Memento永远不共享同一个可变对象。状态嵌套更深时如MapString, ListOrder浅层new ArrayList(...)还不够——里层元素仍是共享引用需要逐层深拷贝。此时可以借助原型模式的clone()或用序列化再反序列化SerializableObjectOutputStream做一次性深拷贝。这也呼应了后文「备忘录 vs 原型」里两者互补的关系。一句话记忆Memento 存的是快照不是引用。只要字段可变创建备忘录时就必须深拷贝。总结本质在不破坏封装性的前提下捕获对象内部状态并支持稍后恢复到该状态。什么时候用需要实现撤销/重做CtrlZ、CtrlY功能需要在特定时机保存对象快照如游戏存档、事务保存点需要回滚到某个历史状态如工作流回退、审批撤回需要在对象状态复杂时避免让外部通过 getter/setter 挨个读写什么时候不用对象状态简单直接备份字段更清晰内存敏感的场景——每个快照都是完整状态副本大对象频繁快照会爆内存状态可以通过反向操作轻松撤销如加法→减法用命令模式更省内存状态变化不需要历史追溯只关心最新值内存管理策略备忘录最大的代价就是内存——每个快照都是一份完整副本。真实项目中常用以下手段控制开销限制历史栈深度Caretaker设一个最大容量如只保留最近 50 步入栈超限时丢弃最老的快照可用ArrayDeque 手动裁剪或LinkedHashMap的 LRU 淘汰增量/差量快照不存完整状态只存相对上一个快照的变化部分类似 Git 的 diff恢复时叠加回放。适合状态大、单次改动小的场景持久化到磁盘对可序列化的状态把较老的快照写到磁盘或数据库释放堆内存需要时再读回游戏存档、事务日志都是这个思路简单记忆状态存进保险箱箱子交给别人扛只有自己能打开历史随时能返场。相似模式区分总览模式核心意图典型场景备忘录保存对象状态快照用于恢复撤销/重做、游戏存档、事务回滚命令将操作请求封装为对象可撤销/排队/记录菜单命令、宏录制、事务日志原型通过克隆现有对象创建新实例对象创建成本高、避免重新初始化简单记忆备忘录管回到过去命令管执行逆操作原型管复制一份。备忘录 vs 命令两者都常用于实现撤销功能但本质不同维度备忘录模式命令模式核心意图保存对象状态快照用于回滚将操作请求封装为对象可执行/撤销结构差异Memento 存状态数据Originator 创建和恢复Command 存行为接收者execute/undoInvoker 调度关注点封装内部状态外部不可见解耦请求发送者与接收者典型场景编辑器内容、游戏存档、事务回滚菜单命令、宏、事务日志逐步区分法如果撤销时只需要恢复成原来的样子→ 选备忘录用状态回滚如果撤销时需要执行反向操作→ 选命令如加法的逆操作是减法如果状态数据量极大如大文本、图像 → 选命令更省内存只记录操作而非完整状态如果操作本身不可逆如发送邮件、扣款 → 只能用备忘录保存操作前的完整状态简单记忆口诀备忘录是时光倒流替换状态命令是撤销操作执行逆操作。推荐状态简单可以计算逆操作时用命令状态复杂或不可逆时用备忘录。两者也可以结合——命令内部持有备忘录撤销时用备忘录恢复状态。备忘录 vs 原型两者都涉及复制对象状态容易混淆维度备忘录模式原型模式核心意图保存状态以备恢复时间维度复制对象以创建新实例空间维度结构差异Memento 是独立快照对象Originator 可读写它Clone 对象是原型的副本各自独立演化关注点封装——外部不应知道保存了什么多态——无需知道具体类即可克隆典型场景撤销/重做、事务回滚、游戏存档对象创建成本高、需要大量相似对象逐步区分法如果需要把对象恢复到之前的状态→ 选备忘录如果需要基于现有对象创建独立的新对象→ 选原型如果既要保存又要新建 → 两者可以结合用原型clone()生成快照作为备忘录的内容简单记忆口诀备忘录问我过去是什么样时间回溯原型问给我一个一样的空间复制。推荐在 Java 中实现备忘录时创建 Memento 经常需要深拷贝 Originator 的状态——此时可以借用原型模式clone()辅助实现快照。两者不是互斥而是互补关系。练习题目计数器的撤销与重做题目描述小明正在设计一个计数器应用支持加 1Increment和减 1Decrement操作以及撤销Undo和重做Redo操作。请使用备忘录模式帮他实现。规则计数器初始值为 0每次执行 Increment 或 Decrement 前需保存当前状态快照到撤销栈并清空重做栈Undo 操作将当前状态保存到重做栈并从撤销栈恢复上一个状态Redo 操作将当前状态保存到撤销栈并从重做栈恢复下一个状态保证输入中 Undo 和 Redo 操作都是合法的撤销栈/重做栈不会为空输入描述输入包含若干行每行一个字符串表示操作Increment、Decrement、Undo、Redo。输出描述每行输出一个整数表示每次操作后计数器的当前值。输入示例Increment Increment Decrement Undo Redo Undo Increment Undo输出示例1 2 1 2 1 2 3 2解题思路本题的核心是理解备忘录模式在撤销/重做中的作用——通过保存快照恢复快照实现状态回溯而不是通过执行逆操作。角色对应Originator发起人Counter持有 count 字段能创建和恢复自身的 MementoMemento备忘录Counter.Memento内部类只保存一个 int 值Caretaker管理者CounterHistory同时维护撤销栈和重做栈核心逻辑Increment/Decrement 之前先save同时清空重做栈——新操作让历史分叉Undo/Redo 都遵循同一个套路先把当前状态保存到对方栈再从当前栈弹出快照并恢复Memento内部类的构造器和 getter 都是privateCounterHistory只能持有它但打不开import java.util.*; public class Main { public static void main(String[] args) { Scanner sc new Scanner(System.in); Counter counter new Counter(0); CounterHistory history new CounterHistory(); while (sc.hasNext()) { String op sc.next(); switch (op) { case Increment: history.save(counter); counter.increment(); System.out.println(counter.getCount()); break; case Decrement: history.save(counter); counter.decrement(); System.out.println(counter.getCount()); break; case Undo: history.undo(counter); System.out.println(counter.getCount()); break; case Redo: history.redo(counter); System.out.println(counter.getCount()); break; } } } } // Originator计数器 class Counter { private int count; public Counter(int count) { this.count count; } public void increment() { count; } public void decrement() { count--; } public int getCount() { return count; } public Memento save() { return new Memento(count); } public void restore(Memento memento) { this.count memento.getCount(); } // Memento备忘录内部类 static class Memento { private final int count; private Memento(int count) { // 私有构造只有 Counter 能创建 this.count count; } private int getCount() { // 私有 getter只有 Counter 能读取 return count; } } } // Caretaker历史管理器 class CounterHistory { private DequeCounter.Memento undoStack new ArrayDeque(); private DequeCounter.Memento redoStack new ArrayDeque(); public void save(Counter counter) { undoStack.push(counter.save()); redoStack.clear(); // 新操作使重做历史失效 }