Java备忘录模式剖析及使用场景

备忘录模式

一、介绍

  • 备忘录模式是一种行为设计模式,它允许在不破坏对象封装性的前提下,捕获并外部化一个对象的内部状态,以便日后可以恢复该对象到之前保存的状态。

  • 假设你在玩一个电子游戏,游戏中有一个保存进度的功能。当你保存游戏时,实际上是将游戏的当前状态(比如关卡、分数、生命值等)存储到一个对象(备忘录)中。当你想恢复到保存的进度时,游戏只需从备忘录对象中读取相关数据,从而将自身恢复到之前的状态。

  • 有个3个角色

    发起者(Originator):需要保存和恢复状态的对象。

    备忘录(Memento):用于存储发起者对象的内部状态。

    管理者(Caretaker):负责存储备忘录对象,但不能修改备忘录的内容。

二、绘图项目中使用备忘录

需求:开发一个绘图应用程序,用户可以在画布上绘制各种形状和线条。我们需要为程序添加"撤销"和"重做"功能,以便用户可以撤销或重做之前的操作。

实现步骤:

  1. 定义备忘录(Memento)类,用于存储画布当前的状态(所有已绘制的形状和线条)。
  2. 定义发起人(Originator)类,表示画布对象,负责创建备忘录对象并从中还原状态。
  3. 定义管理者(Caretaker)类,负责存储多个备忘录对象,以便在需要时恢复到特定状态。
  4. 在发起人(Originator)类中,实现创建备忘录对象和从备忘录对象还原状态的方法。
  5. 在管理者(Caretaker)类中,实现存储和获取备忘录对象的方法。
  6. 在应用程序的主逻辑中,调用发起人和管理者的相关方法,实现"撤销"和"重做"功能。
java 复制代码
// 备忘录类,存储画布当前状态
class Memento {
    private final List<Shape> shapes; // 存储所有已绘制的形状

    public Memento(List<Shape> shapes) {
        this.shapes = new ArrayList<>(shapes);
    }

    public List<Shape> getShapes() {
        return new ArrayList<>(shapes);
    }
}

// 发起人类,表示画布对象
class Canvas {
    private final List<Shape> shapes = new ArrayList<>(); // 存储已绘制的形状

    public void addShape(Shape shape) {
        shapes.add(shape);
    }

    public void removeShape(Shape shape) {
        shapes.remove(shape);
    }

    public Memento createMemento() {
        return new Memento(shapes);
    }

    public void restoreFromMemento(Memento memento) {
        shapes.clear();
        shapes.addAll(memento.getShapes());
    }
}

// 管理者类,负责存储多个备忘录对象
class CareTaker {
    private final Deque<Memento> mementos = new LinkedList<>();
    private final int maxSize = 10; // 最多存储10个备忘录对象

    public void addMemento(Memento memento) {
        mementos.offerLast(memento);
        if (mementos.size() > maxSize) {
            mementos.pollFirst(); // 移除最早的备忘录对象
        }
    }

    public Memento getUndo() {
        return mementos.pollLast();
    }

    public Memento getRedo() {
        return mementos.peekLast();
    }
}

// 形状类,表示在画布上绘制的形状
class Shape {
    private final String type;
    private final int x, y;

    public Shape(String type, int x, int y) {
        this.type = type;
        this.x = x;
        this.y = y;
    }

    // 省略 getter 方法...
}


public class Main {
    public static void main(String[] args) {
        Canvas canvas = new Canvas();
        CareTaker careTaker = new CareTaker();

        // 绘制几个形状
        canvas.addShape(new Shape("Rectangle", 10, 10));
        canvas.addShape(new Shape("Circle", 20, 20));
        careTaker.addMemento(canvas.createMemento()); // 保存当前状态

        canvas.addShape(new Shape("Triangle", 30, 30));
        careTaker.addMemento(canvas.createMemento()); // 保存当前状态

        // 撤销一步
        Memento undoMemento = careTaker.getUndo();
        if (undoMemento != null) {
            canvas.restoreFromMemento(undoMemento);
        }

        // 重做一步
        Memento redoMemento = careTaker.getRedo();
        if (redoMemento != null) {
            canvas.restoreFromMemento(redoMemento);
            careTaker.addMemento(redoMemento); // 添加重做的状态
        }

        // 输出当前画布上的形状
        for (Shape shape : canvas.getShapes()) {
            System.out.println(shape);
        }
    }
}

我们定义了Memento类来存储画布的当前状态,Canvas类作为发起人管理所有已绘制的形状,并提供创建和恢复备忘录的方法。CareTaker类负责存储多个备忘录对象,以便可以撤销和重做操作。Main类展示了如何使用这些类来实现"撤销"和"重做"功能。

三、HashMap中使用

在Java中,java.util.HashMap的实现中使用了备忘录模式。当HashMap需要进行rehash操作(例如在put操作导致容量超过阈值时)时,它会创建一个新的更大的底层数组,并将原有数组中的元素迁移到新数组中。为了避免重复计算元素的哈希值,HashMap会在迁移元素时,将元素的哈希值作为一个"备忘录"保存在Node对象中。

下面是HashMapNode内部类的源码:

java 复制代码
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;

    Node(int hash, K key, V value, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }

    public final K getKey()        { return key; }
    public final V getValue()      { return value; }
    public final String toString() { return key + "=" + value; }

    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    public final boolean equals(Object o) {
        if (o == this)
            return true;
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
            if (Objects.equals(key, e.getKey()) &&
                Objects.equals(value, e.getValue()))
                return true;
        }
        return false;
    }
}

可以看到,Node对象存储了键值对以及键的哈希值。当HashMap需要进行rehash操作时,它会遍历原有数组,根据元素的哈希值计算在新数组中的位置,并将元素复制到新位置,而无需重新计算哈希值。这样做可以提高rehash操作的效率。

四、备忘录模式的优缺点及使用经验

优点:

  1. 保护对象的封装性: 备忘录模式将对象的状态信息封装在备忘录对象中,而不是直接暴露对象的内部状态,从而保护了对象的封装性。
  2. 简化发起人类: 由于状态信息被存储在备忘录对象中,因此发起人类可以专注于实现自身的功能逻辑,而无需处理状态存储和恢复的复杂逻辑。
  3. 支持撤销/重做操作: 备忘录模式天生支持撤销和重做操作,只需存储多个备忘录对象即可。

缺点:

  1. 占用额外内存空间: 每次创建备忘录对象时,都需要消耗一定的内存空间来存储状态信息,如果频繁创建备忘录对象,可能会导致内存占用过高。
  2. 存在效率问题: 如果对象的状态信息过于庞大,创建和存储备忘录对象可能会影响系统的效率。

使用经验:

  1. 不要滥用备忘录模式: 备忘录模式虽然提供了方便的状态存储和恢复功能,但也会带来一定的性能和内存开销。因此,在非必要场景下,不建议使用备忘录模式。
  2. 评估状态信息的大小: 在使用备忘录模式前,应该评估对象状态信息的大小,如果状态信息过于庞大,可能会导致性能和内存问题。
  3. 考虑使用其他模式: 在某些场景下,备忘录模式可能不是最佳选择。例如,如果只需要存储和恢复少量状态信息,可以考虑使用观察者模式或状态模式等其他设计模式。
  4. 合理管理备忘录对象: 为了避免占用过多内存,可以限制存储备忘录对象的数量,或者在适当时机清理无用的备忘录对象。
  5. 封装备忘录对象: 为了保护对象的封装性,备忘录对象应该只能由发起人类访问和修改,而不能被其他类直接访问。

备忘录模式在需要存储和恢复对象状态的场景中非常有用,但也需要合理使用,避免滥用导致性能和内存问题。

相关推荐
爱敲键盘的猴子7 分钟前
深入理解 Java HashMap
java·hashmap
霸道流氓气质8 分钟前
Java开发者AI编程常用MCP、Skills、Rules全景汇总
java·开发语言·ai编程
天涯醉青楼20 分钟前
Spring Boot 接口返回 Bean 时字段名“变形”的深度剖析与解决方案
java
步行cgn37 分钟前
MyBatis 高级映射与延迟加载完全指南
java·tomcat·mybatis
thefool11226638 分钟前
从中序与后序遍历序列构造二叉树
java
吴声子夜歌1 小时前
Java面试题——JVM(二)
java·开发语言·jvm
CRMEB1 小时前
商城大促活动策划全流程:从定目标到复盘四阶段
java·开发语言·人工智能·ai·开源·php
denglinqings1 小时前
Java开发从入门到精通所有课程
java·开发语言
星空1 小时前
maven镜像
java·maven
chuan.bai2 小时前
Java RAG 实战附录:qwen3 与 bge-m3 模型切换指南
java·人工智能·算法