原型模式:快速复制已有实例创建新的实例

492 阅读5分钟

原型模式(Prototype Pattern)定义:用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型相同或相似的新对象

场景

在《英雄联盟》这款游戏里,有很多小兵。我们定义小兵类:

// Minion.java
public class Minion{
    /**
     * 小兵类型:近战小兵MeleeMinion、远程小兵CasterMinion、攻城小兵SiegeMinion、超级兵SuperMinion
     */
    private String type;

    /**
     * 颜色
     */
    private String color;

    /**
     * 血量
     */
    private int hp;

    /**
     * 武器
     */
    private Weapon weapon;

    public Minion(String type, String color, int hp) {
        System.out.println("开始构造小兵");
        this.type = type;
        this.color = color;
        this.hp = hp;
    }
}

我们创建一个小兵,只需要这样Minion minion = new Minion("Melee", "Blue", 200);

在每一波兵中,都有五个这样小兵,我们就需要new 五个对象。如果这个构造方法是一个非常复杂或耗性能的操作,那我们创建五个对象过程中,就消耗大量系统资源。

在这种场景中,就非常适合原型模式:用一个已经创建的小兵作为原型,通过复制该小兵来创建和这个小兵相同或相似的其他小兵。

实现方式

原型模式的核心就是通过复制现有的实例创建新的实例。在Java的Object类中,clone()方法为我们提供了复制对象的功能。

protected native Object clone() throws CloneNotSupportedException;

具体的实现方式,只需要在类中实现Cloneable接口(若类没有实现Cloneable接口,调用clone方法将抛出CloneNotSupportedException异常)。

// 小兵类,实现Cloneable接口
public class Minion implements Cloneable{
    /**
     * 小兵类型:近战小兵MeleeMinion、远程小兵CasterMinion、攻城小兵SiegeMinion、超级兵SuperMinion
     */
    private String type;

    /**
     * 颜色
     */
    private String color;

    /**
     * 血量
     */
    private int hp;

    public Minion(String type, String color, int hp) {
        System.out.println("开始构造小兵");
        this.type = type;
        this.color = color;
        this.hp = hp;
    }

    @Override
    public Minion clone() {
        try {
            // 调用Object类clone方法即可
            Minion copy = (Minion) super.clone();
            return copy;
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
            return null;
        }
    }

    public void display() {
        System.out.println("我是" + type + "小兵" + ";颜色:" + color + ";血量:" + hp);
    }

    public Weapon getWeapon() {
        return weapon;
    }

    public void setWeapon(Weapon weapon) {
        this.weapon = weapon;
    }

    public static void main(String[] args) throws Exception{
        Minion minion = new Minion("Melee", "Blue", 200);
        Minion minionCopy =  minion.clone();
        System.out.println("minion == minionCopy:" + (minion == minionCopy));
        minionCopy.display();
    }
}
// 输出结果:
开始构造小兵
minion == minionCopy:false
我是Melee小兵;颜色:Blue;血量:200

从输出结果我们可以看出:

  • 调用clone方法创建对象跟原型对象不是同一个对象
  • clone方法创建对象不调用类的构造方法(从输出结果中只打印一次「开始构造小兵」可以看出)

浅拷贝&深拷贝

拷贝对象(clone),可以分为两种拷贝方式:

浅拷贝:拷贝对象的引用,而不是copy对象本身

深拷贝:拷贝对象的本身,即会创建一个新的对象

java提供的clone方法,实现的是浅拷贝。我们可以实际验证一下:

还是刚才的Minion类,现在给小兵加一个武器Weapon

// 武器类
public class Weapon implements Cloneable{
    /**
     * 武器类型
     */
    private String type;

    /**
     * 武器是否损坏
     */
    private boolean isDamage;

    public Weapon(String type) {
        this.type = type;
        isDamage = false;
    }

    /**
     * 武器损坏
     */
    public void destroy() {
        this.isDamage = true;
    }

    /**
     * 武器是否损坏
     * @return
     */
    public boolean isDamage() {
        return isDamage;
    }
}

// 小兵类
public class Minion implements Cloneable{
    
    /**
     * 武器
     */
    private Weapon weapon;


    public Weapon getWeapon() {
        return weapon;
    }

    public void setWeapon(Weapon weapon) {
        this.weapon = weapon;
    }

    public static void main(String[] args) throws Exception{
        Minion minion = new Minion("Melee", "Blue", 200);
        Weapon weapon = new Weapon("刀");
        minion.setWeapon(weapon);
        Minion minionCopy =  minion.clone();

        System.out.println("复制小兵1武器是否损坏:" + minionCopy.getWeapon().isDamage());
        // 原始小兵武器损坏
        minion.getWeapon().destroy();
        System.out.println("复制小兵1武器是否损坏:" + minionCopy.getWeapon().isDamage());
    }
}

// 输出结果
开始构造小兵
复制小兵1武器是否损坏:false
复制小兵1武器是否损坏:true

在main方法中代码中,我们只让原始小兵武器损坏,但从输出结果来看,我们复制小兵的武器也坏了。这就印证了我们上面的结论:java提供的clone方法,实现的是浅拷贝。

如何实现深拷贝?

其实很简单,我们可以手动创建需要深拷贝的对象

public Minion clone() {
        try {
            Minion copy = (Minion) super.clone();
            // 手动创建对象
            Weapon weapon = new Weapon(this.getWeapon().getType());
            copy.setWeapon(weapon);
            return copy;
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
            return null;
        }
    }

这种手动创建的方式违背了我们克隆对象的初衷,我们可以改成:对需要拷贝的成员对象再次克隆

// 武器对象实现Cloneable接口
public class Weapon implements Cloneable{
    @Override
    public Weapon clone() {
        try {
            return (Weapon) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
            return null;
        }
    }
}

// 小兵类的clone方法
    @Override
    public Minion clone() {
        try {
            Minion copy = (Minion) super.clone();
            // 克隆对象
            Weapon weapon = this.weapon.clone();
            copy.setWeapon(weapon);
            return copy;
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
            return null;
        }
    }

这种克隆对象方式,有时实现起来会特别复杂,比如有大量的非基本数据类型的成员对象;成员对象的引用嵌套非常深,需要对每层都实现clone方法。

更通用的实现深拷贝的方式:通过序列化和反序列化的方式实现对象的拷贝

Java序列化是指将Java对象转为字节序列的过程,反序列化为把字节序列恢复为Java对象的过程。

具体代码:

    @Override
    public Minion clone() {
        // 序列化、反序列化方式
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        try {

            ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
            objectOutputStream.writeObject(this);

            ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
            ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
            return (Minion) objectInputStream.readObject();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return null;
    }

!!! 将要序列化的对象及其成员变量一定要实现Serializable接口,否则序列化时将抛出ava.io.NotSerializableException异常。

原型模式的优点

  • 隐藏创建新对象的复杂性
  • 可以创建未知类型的对象(直接调用clone方法,不关心对象类型)
  • 可以跳过构造函数,快速创建对象(有时候也会成为其缺点,跳过构造函数执行的逻辑)