设计模式之原型模式

原型模式

一:原型模式概述

为什么要用原型模式:

在系统中有时候可能需要创建多个一模一样的对象,而有的对象创建过程十分复杂,或者创建对象很耗费资源亦或是创建对象十分频繁,那么这个时候就必须要解决这个问题,而原型模式则能很好的解决这个问题。

基本定义:

原型模式(Prototype Pattern):使用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。原型模式是一种对象创建型模式。

二:原型模式原理结构图

1545023759228

基本角色

  • Prototype(抽象原型类)
    是声明了克隆方法的接口,所有具体原型类的基类,既可以是接口又可以是抽象类,还可以是具体实现类。

  • ConcretePrototype(具体原型类)
    实现抽象原型类中的克隆方法,在克隆方法中返回自己一个克隆对象。

三:深克隆与浅克隆

基本概念

  1. 浅复制(浅克隆)

被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。换言之,浅复制仅仅复制所拷贝的对象,而不复制它所引用的对象。

  1. 深复制(深克隆)

    被复制对象的所有变量都含有与原来的对象相同的值,除去那些引用其他对象的变量。那些引用其他对象的变量将指向被复制过的新对象,而不再是原有的那些被引用的对象。换言之,深复制把要复制的对象所引用的对象都复制了一遍。

实现java深复制和浅复制的最关键的就是要实现Cloneable接口中的clone()方法

如何使用clone()方法

首先我们来看一下Cloneable接口:

官方解释:

1:实现此接口则可以使用java.lang.Object 的clone()方法,否则会抛出CloneNotSupportedException 异常

2:实现此接口的类应该使用公共方法覆盖clone方法

3:此接口并不包含clone 方法,所以实现此接口并不能克隆对象,这只是一个前提,还需覆盖上面所讲的clone方法。

public interface Cloneable {
}

看看Object里面的Clone()方法:

  1. clone()方法返回的是Object类型,所以必须强制转换得到克隆后的类型
  2. clone()方法是一个native方法,而native的效率远远高于非native方法,
  3. 可以发现clone方法被一个Protected修饰,所以可以知道必须继承Object类才能使用,而Object类是所有类的基类,也就是说所有的类都可以使用clone方法
protected native Object clone() throws CloneNotSupportedException;

小试牛刀:

public class Person {
    public void testClone(){
        super.clone(); // 报错了
    }
}

事实却是clone()方法报错了,那么肯定奇怪了,既然Object是一切类的基类,并且clone的方法是Protected的,那应该是可以通过super.clone()方法去调用的,然而事实却是会抛出CloneNotSupportedException异常,官方解释如下:

  1. 对象的类不支持Cloneable接口
  2. 覆盖方法的子类也可以抛出此异常表示无法克隆实例。

所以我们更改代码如下:

public class Person implements Cloneable{
    public void testClone(){
        try {
            super.clone();
            System.out.println("克隆成功");
        } catch (CloneNotSupportedException e) {
            System.out.println("克隆失败");
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        Person p = new Person();
        p.testClone();
    }
}

要注意,必须将克隆方法写在try-catch块中,因为clone方法会把异常抛出,当然程序也要求我们try-catch。

java.lang.object规范中对clone方法的约定

  1. 对任何的对象x,都有x.clone() !=x 因为克隆对象与原对象不是同一个对象
  2. 对任何的对象x,都有x.clone().getClass()= =x.getClass()//克隆对象与原对象的类型一样
  3. 如果对象x的equals()方法定义恰当,那么x.clone().equals(x)应该成立

对于以上三点要注意,这3项约定并没有强制执行,所以如果用户不遵循此约定,那么将会构造出不正确的克隆对象,所以根据effective java的建议:

谨慎的使用clone方法,或者尽量避免使用。

浅复制实例

  1. 对象中全部是基本类型
public class Teacher implements Cloneable{
    private String name;
    private int age;

    public Teacher(String name,int age){
        this.name = name;
        this.age = age;
    }
    // 覆盖
    @Override
    public Object clone() {
        try {
            return super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
            return null;
        }
    }
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
// 客户端测试
public class test {
    Teacher origin = new Teacher("tony",11);
    System.out.println(origin.getName());
    Teacher clone = (Teacher) origin.clone();
    clone.setName("clone");
    System.out.println(origin.getName());
    System.out.println(clone.getName());
}

结果:

tony
tony
clone

1545111039834

从运行结果和图上可以知道,克隆后的值变量会开辟新的内存地址,克隆对象修改值不会影响原来对象。

  1. 对象中含有引用类型
public class Teacher implements Cloneable{
    private String name;
    private int age;
    private Student student;

    public Teacher(String name,int age,Student student){
        this.name = name;
        this.age = age;
        this.student = student;
    }
    // 覆盖
    @Override
    public Object clone() {
        try {
            return super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
            return null;
        }
    }
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public Student getStudent() {
        return student;
    }

    public void setStudent(Student student) {
        this.student = student;
    }
}

// 学生类
public class Student {
    private String name;
    private int age;
    public Student(String name,int age){
        this.name = name;
        this.age = age;
    }
     public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
// 客户端测试
public class test {
    public static void main(String[] args) {
        Student student = new Student("学生1",11);
        Teacher origin = new Teacher("老师",11,student);;
        Teacher clone = (Teacher) origin.clone();
        System.out.println("比较克隆后的引用对象");
        System.out.println(origin.getStudent().getClass() == clone.getStudent().getClass());
        Student student2 = new Student("学生2",12);
        clone.setStudent(student2);
        System.out.println("克隆后,比较克隆对象改变引用");
        System.out.println(origin.getStudent().getClass() == clone.getStudent().getClass());
    }
}

运行结果:

比较克隆后的引用对象
true
克隆后,比较克隆对象改变引用
true

1545109406152

如图可知,引用类型只会存在一份内存地址,执行object的clone方法拷贝的也是引用的复制(这部分的内存空间不一样,)但是引用指向的内存空间是一样的,原对象修改引用变量或者浅拷贝对象修改引用变量都会引起双方的变化

重点:综上两个方面可以知道,Object的clone方法是属于浅拷贝,基本变量类型会复制相同值,而引用变量类型也是会复制相同的引用。

深复制实例

从上面的浅拷贝可以知道,对于引用的变量只会拷贝引用指向的地址,也就是指向同一个内存地址,但是很多情况下我们需要的是下面图的效果:

1545121868853

深拷贝实现的是对所有可变(没有被final修饰的引用变量)引用类型的成员变量都开辟内存空间所以一般深拷贝对于浅拷贝来说是比较耗费时间和内存开销的。

深拷贝的两种方式:

  1. 重写clone方法实现深拷贝

学生类:

public class Student implements Cloneable {
    private String name;
    private int age;
    public Student(String name,int age){
        this.name = name;
        this.age = age;
    }

    @Override
    protected Object clone()  {
        try {
            return super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
            return null;
        }
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

老师类:

public class Teacher implements Cloneable{
    private String name;
    private int age;
    private Student student;

    public Teacher(String name,Student student){
        this.name = name;
        this.age = age;
        this.student = student;
    }
    // 覆盖
    @Override
    public Object clone() {
        Teacher t = null;
        try {
            t = (Teacher) super.clone();
            t.student = (Student)student.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return t;
    }
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public Student getStudent() {
        return student;
    }

    public void setStudent(Student student) {
        this.student = student;
    }
}

测试端:

public class test {
    public static void main(String[] args) {
        Student s = new Student("学生1",11);
        Teacher origin = new Teacher("老师原对象",23,s);
        System.out.println("克隆前的学生姓名:" + origin.getStudent().getName());
        Teacher clone = (Teacher) origin.clone();
        // 更改克隆后的学生信息 更改了姓名
        clone.getStudent().setName("我是克隆对象更改后的学生2");
        System.out.println("克隆后的学生姓名:" + clone.getStudent().getName());
    }
}

运行结果:

克隆前的学生姓名:学生1
克隆后的学生姓名:我是克隆对象更改后的学生2

  1. 序列化实现深克隆

我们发现上面通过object的clone方法去实现深克隆十分麻烦, 因此引出了另外一种方式:序列化实现深克隆

概念:

  • 序列化:把对象写到流里
  • 反序列化:把对象从流中读出来

在Java语言里深复制一个对象,常常可以先使对象实现Serializable接口,然后把对象(实际上只是对象的一个拷贝)写到一个流里,再从流里读出来,便可以重建对象。

注意:

  • 写在流里的是对象的一个拷贝,而原对象仍然存在于JVM里面
  • 对象以及对象内部所有引用到的对象都是可序列化的
  • 如果不想序列化,则需要使用transient来修饰

案例:

Teacher:

public class Teacher implements Serializable{
    private String name;
    private int age;
    private Student student;

    public Teacher(String name,Student student){
        this.name = name;
        this.age = age;
        this.student = student;
    }
    // 深克隆
    public Object deepClone() throws IOException,ClassNotFoundException {
        // 序列化
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(this);
        // 反序列化
        ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bis);
        return ois.readObject();
    }
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public Student getStudent() {
        return student;
    }

    public void setStudent(Student student) {
        this.student = student;
    }

Student:

public class Student implements Serializable {
    private String name;
    private int age;
    public Student(String name,int age){
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

client:

public class test {
    public static void main(String[] args) {
        try {
            Student s = new Student("学生1",11);
            Teacher origin = new Teacher("老师原对象",s);
            System.out.println("克隆前的学生姓名:" + origin.getStudent().getName());
            Teacher clone = (Teacher) origin.deepClone();
            // 更改克隆后的d学生信息 更改了姓名
            clone.getStudent().setName("我是克隆对象更改后的学生2");
            System.out.println("克隆后的学生姓名:" + clone.getStudent().getName());
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

当然这些工作都有现成的轮子了,借助于Apache Commons可以直接实现:

  • 浅克隆:BeanUtils.cloneBean(Object obj);
  • 深克隆:SerializationUtils.clone(T object);

最后探讨

  • 在java中为什么实现了Cloneable接口,就可以调用Object中的Clone方法

    参考以下回答:

https://www.zhihu.com/question/52490586

四:原型模式案例

场景如下:新生入学,每个人都要填写自己的个人信息:包括姓名,年龄,身高等等

模板表格:

public class Sheet implements Cloneable {
    private String name;
    private int age;
    private int height;

    // 初始化个人信息
    public Sheet(String name,int height) {
        this.name = name;
        this.age = age;
        this.height = height;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public int getHeight() {
        return height;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    // 提供克隆该实例的方法(浅克隆)
    @Override
    protected Object clone()  {
        try {
            return super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
            return null;
        }
    }

    @Override
    public String toString() {
        return "Sheet{" +
                "name='" + name + '\'' +
                ",age=" + age +
                ",height=" + height +
                '}';
    }
}

客户端:

public class Client {
    public static void main(String[] args) {
        Sheet sheet = new Sheet("我是模板",0);
        Sheet sheet1 = (Sheet) sheet.clone();
        sheet1.setName("学生1");
        sheet1.setAge(11);
        sheet1.setHeight(11);
        System.out.println(sheet1.toString());

        Sheet sheet2 = (Sheet) sheet.clone();
        sheet2.setName("学生2");
        sheet2.setAge(11);
        sheet2.setHeight(11);
        System.out.println(sheet2.toString());

    }
}

运行结果:

Sheet{name='学生1',age=11,height=11}
Sheet{name='学生2',height=11}

四:原型模式总结

  • 优点
    • 如果要创建的对象实例比较复杂,那么用原型模式可以简化其创建过程,减少内存开销,提高效率。
    • 可以使用深克隆的方式保存对象的状态,通过原型模式将其状态保存起来,以便需要时候使用。
  • 缺点
    • 需要为每个类提供克隆方式,极其麻烦,当要扩展克隆方法时候,必须修改源代码,违反开闭原则。
    • 在实现深克隆的时候需要写大量代码,运用起来极其麻烦。

五:使用场景

  1. 创建新对象成本较大(如初始化需要占用较长的时间,占用太多的CPU资源或网络资源),新的对象可以通过原型模式对已有对象进行复制来获得,如果是相似对象,则可以对其成员变量稍作修改。

相关文章

什么是设计模式一套被反复使用、多数人知晓的、经过分类编目...
单一职责原则定义(Single Responsibility Principle,SRP)...
动态代理和CGLib代理分不清吗,看看这篇文章,写的非常好,强...
适配器模式将一个类的接口转换成客户期望的另一个接口,使得...
策略模式定义了一系列算法族,并封装在类中,它们之间可以互...
设计模式讲的是如何编写可扩展、可维护、可读的高质量代码,...