Java备忘录模式实战案例详解:从原理到代码实现
目录导读
- 什么是备忘录模式?——核心概念与生活类比
- 备忘录模式的UML结构与角色解析
- Java实现备忘录模式的三种经典案例
- 文本编辑器的撤销功能
- 游戏角色状态存档
- 数据库事务回滚模拟
- 备忘录模式在Java中的常见陷阱与优化
- 问答环节:开发者最常遇到的5个问题
什么是备忘录模式?——核心概念与生活类比
备忘录模式(Memento Pattern)是一种行为型设计模式,它允许在不破坏对象封装性的前提下,捕获并外部化一个对象的内部状态,以便在后续需要时将该对象恢复到先前保存的状态,简单说,就是“后悔药”机制。

生活类比:
想象你在玩《塞尔达传说》——在挑战Boss前,你手动存档,如果打Boss失败,你可以读档回到挑战前的状态,这里的存档文件就是“备忘录”,游戏主角是“发起人”,而存档系统是“管理者”。
核心三要素:
- 发起人(Originator):负责创建备忘录并利用备忘录恢复自身状态。
- 备忘录(Memento):存储发起人内部状态的对象,对外部不可修改。
- 管理者(Caretaker):负责保存备忘录,但不能修改其内容。
适用场景:
- 需要保存对象在某一时刻的完整状态(快照)
- 实现撤销/重做功能(如IDE的Ctrl+Z)
- 需要避免直接暴露对象内部状态给外部
备忘录模式的UML结构与角色解析
┌─────────────────────┐ ┌─────────────────────┐
│ Originator │ │ Memento │
│─────────────────────│ │─────────────────────│
│ - state: String │◄─────────│ - state: String │
│ + saveState(): void │ │ + getState(): void │
│ + restore(m): void │ └─────────────────────┘
└─────────────────────┘ ▲
│ │
│ 创建/恢复 │
▼ │
┌─────────────────────┐ │
│ Caretaker │ │
│─────────────────────│ │
│ - mementos: List │─────────► 持有备忘录
│ + addMemento(m) │
│ + getMemento(i) │
└─────────────────────┘
关键设计原则:
- 备忘录对象应限制访问权限,仅允许发起人读写其内容,而对管理者只提供只读接口。
- 在Java中,通常通过嵌套类或包级私有来实现封装。
Java实现备忘录模式的三种经典案例
文本编辑器的撤销功能
需求:实现一个简单的文本编辑器,支持保存当前内容,并能够恢复到历史版本。
// 1. 备忘录类(内部类形式)
public class Editor {
private String content;
public void setContent(String content) {
this.content = content;
}
public String getContent() {
return content;
}
// 保存快照
public Memento save() {
return new Memento(content);
}
// 恢复快照
public void restore(Memento memento) {
this.content = memento.getContent();
}
// 内部备忘录类,对外隐藏实现细节
public static class Memento {
private final String content;
private Memento(String content) {
this.content = content;
}
private String getContent() { // 私有方法,仅Editor可调用
return content;
}
}
}
// 2. 管理者(历史记录)
import java.util.Stack;
public class History {
private Stack<Editor.Memento> stack = new Stack<>();
public void push(Editor.Memento memento) {
stack.push(memento);
}
public Editor.Memento pop() {
return stack.isEmpty() ? null : stack.pop();
}
}
// 3. 客户端测试
public class Client {
public static void main(String[] args) {
Editor editor = new Editor();
History history = new History();
editor.setContent("第一版内容");
history.push(editor.save());
editor.setContent("第二版内容(错误)");
history.push(editor.save());
editor.setContent("第三版内容");
// 发现第二版错了,撤销到第一版
editor.restore(history.pop()); // 恢复到“第二版内容”
editor.restore(history.pop()); // 恢复到“第一版内容”
System.out.println(editor.getContent()); // 输出:第一版内容
}
}
输出:
关键点:使用Stack实现“最近撤销”,备忘录类作为内部类确保私有状态不被外界篡改。
游戏角色状态存档
需求:一个RPG游戏角色有生命值、魔法值和位置属性,要求支持存档和读档。
public class GameCharacter {
private int hp;
private int mp;
private int x, y;
public GameCharacter(int hp, int mp, int x, int y) {
this.hp = hp;
this.mp = mp;
this.x = x;
this.y = y;
}
public void setPosition(int x, int y) {
this.x = x;
this.y = y;
}
public void receiveDamage(int damage) {
this.hp = Math.max(0, hp - damage);
}
// 创建备忘录
public CharacterMemento saveProgress() {
return new CharacterMemento(hp, mp, x, y);
}
// 恢复进度
public void loadProgress(CharacterMemento memento) {
this.hp = memento.getHp();
this.mp = memento.getMp();
this.x = memento.getX();
this.y = memento.getY();
}
@Override
public String toString() {
return String.format("HP:%d MP:%d 位置:(%d,%d)", hp, mp, x, y);
}
// 备忘录,注意访问权限
public static class CharacterMemento {
private final int hp, mp, x, y;
private CharacterMemento(int hp, int mp, int x, int y) {
this.hp = hp;
this.mp = mp;
this.x = x;
this.y = y;
}
// 仅对外提供getter(管理者无权修改)
public int getHp() { return hp; }
public int getMp() { return mp; }
public int getX() { return x; }
public int getY() { return y; }
}
}
// 存档管理器
public class SaveManager {
private Map<String, GameCharacter.CharacterMemento> saves = new HashMap<>();
public void save(String slot, GameCharacter.CharacterMemento memento) {
saves.put(slot, memento);
}
public GameCharacter.CharacterMemento load(String slot) {
return saves.get(slot);
}
}
// 客户端
GameCharacter hero = new GameCharacter(100, 50, 0, 0);
SaveManager manager = new SaveManager();
hero.receiveDamage(30); // 战斗后HP为70
manager.save("save1", hero.saveProgress());
hero.setPosition(10, 20);
hero.receiveDamage(50); // 再受伤害HP为20
// Boss战前读档
hero.loadProgress(manager.load("save1"));
System.out.println(hero); // HP:70 MP:50 位置:(0,0)
输出:HP:70 MP:50 位置:(0,0)
数据库事务回滚模拟
需求:模拟一个简单的银行转账场景,如果转账失败需回滚账户余额。
public class BankAccount {
private double balance;
public BankAccount(double balance) {
this.balance = balance;
}
public void debit(double amount) {
if (amount > 0 && balance >= amount) {
balance -= amount;
}
}
public void credit(double amount) {
if (amount > 0) {
balance += amount;
}
}
public AccountMemento save() {
return new AccountMemento(balance);
}
public void restore(AccountMemento m) {
this.balance = m.getBalance();
}
public double getBalance() { return balance; }
// 备忘录类
public static class AccountMemento {
private final double balance;
private AccountMemento(double balance) {
this.balance = balance;
}
private double getBalance() { return balance; }
}
}
// 事务管理器
public class TransactionManager {
public boolean transfer(BankAccount from, BankAccount to, double amount) {
BankAccount.AccountMemento mementoFrom = from.save();
BankAccount.AccountMemento mementoTo = to.save();
try {
from.debit(amount);
to.credit(amount);
// 模拟失败场景(如余额不足)
if (from.getBalance() < 0) {
throw new RuntimeException("余额不足");
}
return true; // 提交
} catch (Exception e) {
// 回滚
from.restore(mementoFrom);
to.restore(mementoTo);
System.out.println("事务回滚:" + e.getMessage());
return false;
}
}
}
要点:在业务操作前保存快照,异常时恢复,保证数据一致性。
备忘录模式在Java中的常见陷阱与优化
陷阱1:备忘录对象过大
当发起人状态包含大量数据(如整个文档),每次保存完整快照会导致内存消耗暴增。
优化方案:
- 使用“差异备忘录”,只保存变更的部分。
- 结合序列化,将备忘录写入磁盘(如游戏存档)。
陷阱2:封装性被破坏
如果备忘录类被定义为public且对外暴露setter,管理者可能非法修改内部状态。
正确做法:
- 使用内部类,并将构造器和getter设为
private(仅发起人可访问)。 - 或者使用接口隔离:定义一个
Memento接口(只有getter),内部实现类实现具体setter。
陷阱3:状态恢复不完整
某些对象包含不可序列化的资源(如数据库连接、文件句柄),备忘录模式无法保存这些。
解决方法:
- 将状态与资源分离,备忘录只保存可序列化部分。
- 或者重写对象的
clone()方法(注意深拷贝问题)。
性能优化技巧
- 使用LruCache管理备忘录数量,避免无限存储。
- 在关键节点(如游戏Boss战前)才保存快照,而非每次动作都存。
问答环节:开发者最常遇到的5个问题
Q1:备忘录模式与原型模式(Prototype)有何区别?
A:原型模式侧重于通过克隆创建新对象,而备忘录模式侧重于保存和恢复对象状态,原型可作用于任意对象,备忘录需要专门设计发起人与管理者的协作关系。
Q2:为什么备忘录类要使用内部类?
A:内部类可以访问外部类的私有成员,同时可以将自己的构造器和方法设为私有,从而只允许外部类(发起人)创建和访问,管理者只能持有引用却无法修改内容。
Q3:在Java中,如何实现“撤销到任意步骤”而非仅上一步?
A:将管理者的数据结构从Stack改为List或Map,并记录每个快照的时间戳或版本号,用户通过索引或ID选择恢复点。
Q4:动态代理可以替代备忘录模式吗?
A:不可以,动态代理主要用于方法拦截,无法自动保存对象内部状态,备忘录模式专注于“快照”语义,与代理职责不同。
Q5:哪些场景不适合备忘录模式?
A:对象状态极其复杂且频繁变化(如实时渲染引擎);对象包含大量无法序列化的资源;或者期望以极低内存开销实现撤销功能(此时应考虑命令模式+反向操作)。
备忘录模式是Java开发中实现“撤销/恢复”功能最常见的方案,其核心在于封装状态快照与职责分离,通过本文的三个案例(文本编辑器、游戏存档、事务回滚),你已掌握从简单到复杂场景的实战技巧,在实际项目中,建议结合Serializable接口或Jackson库来序列化复杂对象的备忘录,同时注意控制快照粒度和存储策略,掌握这个模式后,你就能为你的程序轻松添加“时间回溯”的超能力了。