浅谈原型模式(Prototype Pattern)

144 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第13天,点击查看活动详情

原型模式属于创建型模式,通过学习前面的创建型模式就该知道,创建型模式最终的目的就是创造一个实例对象出来至于用什么方法,各个创建型模式都不一样。

而原型模式就是通过复制一个已经创建好的对象来获取相同或者类似的对象,为什么要这样干,因为在某些场景下,直接闯将一个实例对象会比较耗费资源,此时通过复制原有对象反而更简单

比如说齐天大圣孙悟空,原创一个齐天大圣需要超长时间,在五指山都被封了五百年,更不要说在灵石里面待的时间了,而从石猴成为齐天大圣之后,孙悟空一根猴毛就可以复制出一个自己

提到复制就不得不提到Java中Object的clone()方法了,这是Java 自带的原型模式,基于内存二进制流的复制,在性能上比直接 new 一个对象更加优良。

而clone翻译过来就是克隆的意思,而克隆则分为浅克隆与深克隆两种设计

(1)浅克隆(Shallow Clone)

在浅克隆中,如果原型对象的成员亦量是值类型(如 int、double、byte、bool 、char 等基本数据类型)将复制一份给克降对象,如果原型对象的成员变量是引用类型(如类、接口、数组等复杂数据类型),则将引用对象的地址复制一份给克降对象,也就是说,原型对象和克隆对象的成员变量指向相同的内存地址。简单来说,在浅克隆中,当原型对象被复制时,只复制它本身和其中包含的值类型的成员变量,而引用类型的成员变量并没有复制。

image.png

(2)深克隆(Deep Clone)

在深克隆中,无论原型对象的成员变量是值类型还是引用类型,都将复制一份给克隆对象,深克隆将原型对象的所有引用对象也复制一份给克隆对象。简单来说,在深克隆中,除了对象本身被复制外,对象所包含的所有成员变量也将被复制。

image.png

那么我们来看看java里面的clone,在java里面的克隆需要实现Cloneable接口并且重写clone()方法。(代码里面的注解都是跟lombok插件有关,@AllArgsConstructor表示有参构造,@NoArgsConstructor表示无参构造,@Setter @Getter表示直接在属性上添加setter与getter方法)

public class TestClass {
    public static void main(String[] args) throws CloneNotSupportedException {
        // 创建一把武器
        Weapon weapon = new Weapon("金箍棒", 200);
        System.out.println("武器为:"+weapon.toString());
        // 创建一个孙悟空
        SunWuKong sunWuKong = new SunWuKong(23, "大闹天宫", weapon);
        System.out.println("孙悟空为:"+sunWuKong.toString());

        // 直接克隆孙悟空
        SunWuKong cloneSunWuKong = (SunWuKong) sunWuKong.clone();
        System.out.println("克隆孙悟空的武器为:"+cloneSunWuKong.getWeapon().toString());
        System.out.println("克隆孙悟空为:"+cloneSunWuKong.toString());
    }
}

// 孙悟空
@AllArgsConstructor
@NoArgsConstructor
class SunWuKong implements Cloneable{
    private int age;
    private String hobby;
    @Getter
    private Weapon weapon;
    
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

}

// 武器
@AllArgsConstructor
@NoArgsConstructor
class Weapon{
    private String name;
    private int length;
}

image.png

直接对比结果发现,孙悟空两个内存地址确实不一致,但是武器内存地址确实一致的,也就是说,武器根本就没有被克隆,也证明了Java里面的clone()方法是属于浅克隆。

那么,在Java里面深克隆要怎么弄?有两种方式

一、需要自己手动添加克隆代码,用上面的例子就是分为两步
(1)武器也需要实现Cloneable接口
(2)孙悟空实现clone()方法时,需要手动克隆武器引用

// 孙悟空
@AllArgsConstructor
@NoArgsConstructor
class SunWuKong implements Cloneable{
    private int age;
    private String hobby;

    @Setter
    @Getter
    private Weapon weapon;


    public Object clone() throws CloneNotSupportedException {
        SunWuKong clone = (SunWuKong) super.clone();
        // 手动克隆
        clone.setWeapon((Weapon) clone.getWeapon().clone());
        return clone;
    }

}

// 武器
@AllArgsConstructor
@NoArgsConstructor
class Weapon implements Cloneable{
    private String name;
    private int length;
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

image.png

从运行结果可以看到孙悟空跟克隆孙悟空内存地址不一样了,武器跟克隆武器也不一样了,证明深克隆成功。

二、通过对象序列化方式来完成

1.序列化方式就是实现Serializable接口,克隆对象跟克隆对象的引用属性都要加上,比如下面的孙悟空跟武器都要实现Serializable接口
2.自定义一个深克隆方法,通过流的方式实现序列化即可

@AllArgsConstructor
@NoArgsConstructor
class SunWuKong implements Serializable {
    private int age;
    private String hobby;

    @Setter
    @Getter
    private Weapon weapon;

    public Object deepClone() throws IOException, ClassNotFoundException {
        //序列化
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
        objectOutputStream.writeObject(this);
        //反序列化
        ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
        ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
        SunWuKong roomClone = (SunWuKong)objectInputStream.readObject();
        return roomClone;
    }

}

@AllArgsConstructor
@NoArgsConstructor
class Weapon implements Serializable {
    private String name;
    private int length;
}

image.png

从运行结果可以看到孙悟空跟克隆孙悟空内存地址不一样了,武器跟克隆武器也不一样了,证明深克隆成功。

再回来谈原型模式:克隆基本上就是原型模式最重要最关键的点了

原型模式包含以下主要角色。

  1. 抽象原型类:规定了具体原型对象必须实现的接口。上面没有定义抽象原型类,如果定义了,那么SunWuKong就要继承这个抽象类或者实现这个抽象接口
  2. 具体原型类:实现抽象原型类的 clone() 方法,它是可被复制的对象。如上面的SunWuKong类
  3. 访问类:使用具体原型类中的 clone() 方法来复制新的对象。如上面的TestClass类

原型模式通常适用于以下场景。

  • 对象之间相同或相似,即只是个别的几个属性不同的时候。
  • 创建对象成本较大,例如初始化时间长,占用CPU太多,或者占用网络资源太多等,需要优化资源。
  • 创建一个对象需要繁琐的数据准备或访问权限等,需要提高性能或者提高安全性。
  • 系统中大量使用该类对象,且各个调用者都需要给它的属性重新赋值。