一、概述
原型模式(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、一个对象多个修改者的场景。