← 返回题目列表

如何用 Java 实现备忘录模式?

高频 中等 第 9 / 25 题 更新于 2026/07/28
备忘录模式Java实现撤销栈

简化版

Java 实现备忘录模式通常让 Originator 提供 save()restore(Memento),Memento 保存状态且尽量不可变,Caretaker 用栈保存历史快照。撤销时从栈中取出备忘录交给 Originator 恢复。

详细版

示例结构:

class Editor {
    private String content;

    public Memento save() {
        return new Memento(content);
    }

    public void restore(Memento memento) {
        this.content = memento.getContent();
    }
}

Memento:

class Memento {
    private final String content;
    public Memento(String content) { this.content = content; }
    String getContent() { return content; }
}

Caretaker:

Stack<Memento> history = new Stack<>();
history.push(editor.save());
editor.restore(history.pop());

实际项目中要注意快照粒度、深拷贝和历史栈大小。

完整版教学

一、Originator 代码

Originator 是业务对象,例如编辑器:

class Editor {
    private String title;
    private String content;

    public Memento save() {
        return new Memento(title, content);
    }

    public void restore(Memento memento) {
        this.title = memento.getTitle();
        this.content = memento.getContent();
    }
}

它自己决定哪些状态进入快照。

二、Memento 设计

Memento 最好设计成不可变:

final class Memento {
    private final String title;
    private final String content;
}

不可变可以防止 Caretaker 或其他外部对象修改历史快照,提高恢复可靠性。

三、Caretaker 代码

Caretaker 可以是历史管理器:

class History {
    private Deque<Memento> undoStack = new ArrayDeque<>();

    public void push(Memento memento) {
        undoStack.push(memento);
    }

    public Memento pop() {
        return undoStack.pop();
    }
}

真实项目中还要处理栈为空、最大容量、redo 栈、异常恢复。

四、常见误区与工程判断

不要让 Memento 暴露 public setter。如果历史快照能被外部修改,撤销功能就不可靠。

如果状态里有 List、Map 或自定义对象,构造 Memento 时要考虑防御性拷贝,否则外部后续修改集合会污染快照。

五、Java 实现时要守住封装边界

Java 示例里经常把 Memento 写成简单 POJO,但真实实现要注意可见性。Memento 的内部状态最好不要被 Caretaker 修改,可以把字段设为 private final,只提供必要元信息读取方法。真正的恢复逻辑由 Originator 的 restore(memento) 完成。

如果状态里包含集合,创建备忘录时要考虑防御性拷贝。例如 new ArrayList<>(items) 可以避免后续修改原集合影响快照;恢复时也要创建新集合,避免恢复后继续共享同一份可变引用。否则代码表面上有 Memento,实际历史状态仍然可能被污染。

另外,示例通常用一个 Stack<Memento>,但工程里更推荐语义清楚的 Deque<Memento>。如果支持 redo,就维护两个 Deque;如果支持历史列表跳转,就用 List 加当前位置索引。数据结构不是死的,关键是历史语义要一致。

六、用工程约束检验答案

Java 示例至少要验证 3 件事:快照不可被外部改写、可变字段与当前对象不共享、恢复后满足对象不变量。若保存一个含 10 个标签的 ArrayList,构造快照和恢复时都复制列表,才能避免后续 add 污染历史。

检查项核心判断工程含义
Memento 类型private static / 包受限限制外部解析
字段final + 不可变副本防止快照污染
restore由 Originator 实现集中校验状态

把关键关系压缩成一条可复述的路径:

new ArrayList<>(currentTags)
       -> Memento 内部私有保存
restore 时再次复制
       -> 当前状态不与快照共享

只把字段设为 final 并不等于深度不可变;final 列表仍能被修改,必须复制或使用不可变值对象。

落地前可以再按下面 3 步复核:

  1. 先说明“Memento 类型”的核心机制:private static / 包受限;再交代边界:限制外部解析。
  2. 接着分析“字段”:final + 不可变副本;不能遗漏对应代价或结果:防止快照污染。
  3. 最后用“restore”检查方案:由 Originator 实现;验收时确认集中校验状态。

这三项构成完整判断链:先讲清Memento 类型,再说明字段,最后用restore检验实现是否越界。

面试中若能给出违反“集中校验状态”的反例,再说明修正办法,答案就从模式定义落到了可验证的工程决策。

七、常见误区与追问

  • 误区:只看到“Memento 类型”就认为方案成立。 必须同时说明核心机制“private static / 包受限”和工程边界“限制外部解析”。
  • 误区:把“字段”当成无条件结论。 只有在“final + 不可变副本”成立时,才能据此讨论“防止快照污染”。
  • 追问:record 适合做 Memento 吗? 适合承载不可变值,但公开访问器可能暴露内容,应结合可见性要求。
  • 追问:为什么恢复时也复制? 避免当前对象后续修改反向污染仍在历史栈中的快照。
  • 追问:Optional 适合表示无快照吗? 可以,但历史为空通常由 Caretaker 的栈状态直接表达。
  • 追问:异常在哪一层处理? 版本或状态校验由 Originator 处理,历史为空由 Caretaker 或调用层处理。
  • 追问:怎样做单元测试? 保存后修改原对象和原集合,再恢复并断言值及引用隔离。

八、加强记忆

记忆时抓住这条主线:Originator 提供 save 和 restore;Memento 尽量不可变;Caretaker 用栈管理历史;集合状态要考虑防御性拷贝。面试回答先给出模式意图,再用调用链或数据流说明角色协作,最后主动交代适用边界与工程代价。