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

一、介绍

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

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

  • 有个3个角色
    发起者(Originator):需要保存和恢复状态的对象。
    备忘录(Memento):用于存储发起者对象的内部状态。
    管理者(Caretaker):负责存储备忘录对象,但不能修改备忘录的内容。

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

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

实现步骤:

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

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. 封装备忘录对象: 为了保护对象的封装性,备忘录对象应该只能由发起人类访问和修改,而不能被其他类直接访问。

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

  • 23
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Java语录精选

你的鼓励是我坚持下去的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值