「设计模式」原型模式

148 阅读3分钟

一、概述

原型模式(Prototype Pattern)是用于创建重复的对象,同时又能保证性能。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式

简单理解就是一个对象的产生可以不由零起步,直接从一个已经具备一定雏形的对象克隆,然后再修改为所需要的对象。

二、优缺点

优点

(1)性能提高。

(2)逃避构造函数的约束。

缺点

(1)配备克隆方法需要对类的功能进行通盘考虑,这对于全新的类不是很难,但对于已有的类不一定很容易,特别当一个类引用不支持串行化的间接对象,或者引用含有循环结构的时候。

(2)必须实现 Cloneable 接口。

三、实现方式

原型模式的实现方式有两种:浅拷贝深拷贝

下面可以通过一个简单的例子来理解原型模式的应用。设计一个学生类,学生类的主要成员变量有:名字(name)、班级(classid)、课程(course),普通方式模拟创建10个学生:

@Data
public class Student{

    private String name;

    private Integer classid;

    private List<String> course = new ArrayList<>();

    public Student(String name, Integer classid) throws InterruptedException {
        this.name = name;
        this.classid = classid;
        //模拟业务逻辑操作消耗10ms
        Thread.sleep(10);
        course.add("语文");
        course.add("数学");
        course.add("英语");
    }
}

测试代码

public class DemoTest {
    @Test
    public void test() throws InterruptedException {
        long start =  new Date().getTime();
        for(int i=0; i<10; i++) {
            Student student = new Student("学生" + (i + 1), 1);
        }
        long end =  new Date().getTime();
        System.out.println("创建10个对象共花费了时间:" + (end -start) + " ms");
        //创建10个对象共花费了时间:151 ms
    }
}

创建10个对象,花费了151ms,如果循环遍历千百级别的数量,消耗的成本太高了。

浅拷贝

创建一个新对象,新对象的属性和原来对象完全相同,对于非基本类型属性,仍指向原有属性所指向的对象的内存地址。

下面是浅拷贝原型模式实现的代码:

@Data
public class Student implements Cloneable{

    //与普通创建部分代码一致

    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

测试代码

public class DemoTest1 {
    @Test
    public void test1() throws InterruptedException, CloneNotSupportedException {
        Student studentA = new Student("学生", 1);
        Student studentB = (Student) studentA.clone();
        System.out.println("StudentA == StudentB : " + (studentA == studentB));
        System.out.println("StudentA.course == StudentB.course : " + (studentA.getCourse() == studentB.getCourse()));
        long start =  new Date().getTime();
        List<Student> students = new ArrayList<>();
        Student student = new Student("学生", 1);
        for(int i=0; i<10; i++) {
            Student studentNew = (Student) student.clone();
            studentNew.setName("学生" +  (i + 1));
            students.add(studentNew);
        }
        long end =  new Date().getTime();
        System.out.println("创建10个对象共花费了时间:" + (end -start) + " ms");
        for (Student one: students) {
            System.out.println(one.toString());
        }
        //如果学生6,转班到班级2,并且多学了一门物理
        for (Student one: students) {
            if(one.getName().equals("学生6")){
                one.setClassid(2);
                one.getCourse().add("物理");
            }
        }
        for (Student one: students) {
            System.out.println(one.toString());
        }
    }
}

可以看到,通过原型模式的方式,同样是创建10个对象,只花了14ms,大大的提高了程序性能。

浅拷贝后,两个对象的地址不相等,但是对象内部的引用类型的地址是相等的,说明是两个独立的对象,内部引用类型是共享的。

但是存在一个问题,明明只是修改了“学生6”的课程,但是其它所有同学的课程都和他一样了。为么会出现这种现象呢?

对于浅拷贝,基本数据类型是值传递,所以修改值后不会影响另一个对象的该属性值;引用数据类型是地址传递(引用传递),所以修改值后另一个对象的该属性值会同步被修改。

String(或Integer)类型非常特殊,所以额外设置了一个字符串类型的成员变量来进行说明。首先,String(或Integer)类型属于引用数据类型,不属于基本数据类型,但是String(或Integer)类型的数据是存放在常量池中的,也就是无法修改的。

深拷贝

创建一个新对象,属性中引用的其他对象也会被克隆,不再指向原有对象地址。

下面通过深拷贝解决上面问题

@Data
public class Student implements Cloneable{

    //与普通创建部分代码一致

    @Override
    public Object clone() throws CloneNotSupportedException {

        Object object = super.clone();
        Student student = (Student)object;

        //拷贝引用类型
        List<String> newCourse = new ArrayList<>();
        Iterator<String> it = student.course.iterator();
        while (it.hasNext()) {
            newCourse.add(it.next());
        }
        student.course  = newCourse;

        return object;
    }
}

测试代码和浅拷贝一致

可以看到,深拷贝模式 ,修改一个对象的引用类型的成员不会再影响另外对象的该成员了。

深拷贝后,两个对象的地址不相等,但是对象内部的引用类型的地址也不相等的,说明是两个独立的对象,内部引用类型也是单独开辟了新的地址空间。

四、常见应用场景

1、资源优化场景。

2、类初始化需要消化非常多的资源,这个资源包括数据、硬件资源等。

3、性能和安全要求的场景。

4、通过 new 产生一个对象需要非常繁琐的数据准备或访问权限,则可以使用原型模式。

5、一个对象多个修改者的场景。