一般我们对于原型模式的定义就是:
用原型实例指定创建对象的种类并且通过拷贝这些原型对象创建新的对象。
简单来说就是一个deep copy。所以我们再讨论原型模式之前,先讨论一下拷贝。
=========================================================================
拷贝
一般在java的拷贝中,分为深拷贝(DeepCopy)和浅拷贝(ShallowCopy)。
浅拷贝:将一个对象拷贝后,基本数据类型的变量都会重新创建,而引用类型,指向的还是原对象所指向的。 简单来说就是增加了一个指针指向已经存在的内存地址,我们改变原来的对象的值,复制的对象的值也会发生改变。
举个例子,假设鸣人会影分身,鸣人本尊的老师是伊鲁卡老师,鸣人的分身的老师也是伊鲁卡老师,所以当我们克隆鸣人这个对象,鸣人本尊的老师变成卡卡西以后,影分身的老师也会变成卡卡西。
看看代码:
首先创建一个教师类,教师类实现Cloneable并且复写super的clone()方法实现克隆。
package Prototype.Dao;
/**
* @author Zeyu Wan
* @version 1.0.0
* @ClassName shallowCopy.java
* @Description 老师类
* @createTime 2022年02月28日 13:24:00
*/
public class Teacher implements Cloneable{
private String name;
private int 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;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
创建一个学生类,实现Cloneable并且复写super的clone()方法实现克隆。
package Prototype.Dao;
/**
* @author Zeyu Wan
* @version 1.0.0
* @ClassName shallowCopy.java
* @Description 学生类
* @createTime 2022年02月28日 13:24:00
*/
public class Student implements Cloneable {
private String name;
private int age;
private Teacher teacher;
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 Teacher getTeacher() {
return teacher;
}
public void setTeacher(Teacher teacher) {
this.teacher = teacher;
}
/**
* @Description 复写clone方法
*/
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
浅拷贝实现:
package Prototype.CopyDemo;
import Prototype.Dao.Student;
import Prototype.Dao.Teacher;
/**
* @author Zeyu Wan
* @version 1.0.0
* @ClassName shallowCopy.java
* @Description 浅拷贝
* @createTime 2022年02月28日 13:24:00
*/
public class ShallowCopy {
public static void main(String[] args) throws CloneNotSupportedException {
Teacher teacher = new Teacher();
teacher.setName("伊鲁卡");
teacher.setAge(28);
Student naruto = new Student();
naruto.setName("鸣人");
naruto.setAge(12);
naruto.setTeacher(teacher);
//输出本体的地址
System.out.println("本体地址: "+naruto);
//输出影分身的地址
System.out.println("本体老师地址: "+naruto.getTeacher());
System.out.println("-----影-------分------身--------");
//获取影分身
Student shadowNaruto = (Student) naruto.clone();
//输出影分身的信息->直接复制了本体
System.out.println("影分身名字: "+shadowNaruto.getName());
//输出影分身老师的信息->直接复制了本体
System.out.println("影分身老师: "+shadowNaruto.getTeacher().getName());
//输出影分身的地址
System.out.println("影分身地址: "+shadowNaruto);
//输出影分身老师的地址
System.out.println("影分身老师地址: "+shadowNaruto.getTeacher());
System.out.println("-------------换老师-------------");
// 修改本体老师的信息
naruto.getTeacher().setName("卡卡西");
System.out.println("鸣人的老师是: " + naruto.getTeacher().getName());
System.out.println("鸣人的影分身老师是: " + shadowNaruto.getTeacher().getName());
}
}
运行结果:
从结果来看,首先本体的地址和影分身的地址是不同的,说明我们的影分身是一个新的对象。
但是本体老师的地址和影分身老师的地址相同。代表了在影分身中,只是多了一个指针指向了原来的老师地址。所以当我们对本体进行换老师的时候,影分身的老师也被换了。
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
深拷贝:
将一个对象拷贝后,不论是基本数据类型还有引用类型,都是重新创建的。简单来说,就是深复制进行了完全彻底的拷贝,而浅拷贝不彻底。clone明显是深拷贝,clone出来的对象是是不能去影响原型对象的。
还是上面的例子,我们增加一个深拷贝的学生类: 我们复写clone()方法,重新set对象。
package Prototype.Dao;
/**
* @author Zeyu Wan
* @version 1.0.0
* @ClassName DeepStudent.java
* @Description 深拷贝学生类
* @createTime 2022年02月28日 15:14:00
*/
public class DeepStudent extends Student{
@Override
public Object clone() throws CloneNotSupportedException {
Student student = (Student) super.clone();
// 本来是浅复制,现在将Teacher对象复制一份并重新set进来
student.setTeacher((Teacher) student.getTeacher().clone());
return student;
}
}
深拷贝实现:
package Prototype.CopyDemo;
import Prototype.Dao.DeepStudent;
import Prototype.Dao.Teacher;
/**
* @author Zeyu Wan
* @version 1.0.0
* @ClassName DeepCopy.java
* @Description 深拷贝
* @createTime 2022年02月28日 15:11:00
*/
public class DeepCopy {
public static void main(String[] args) throws CloneNotSupportedException {
Teacher teacher = new Teacher();
teacher.setName("伊鲁卡");
teacher.setAge(28);
DeepStudent naruto = new DeepStudent();
naruto.setName("鸣人");
naruto.setAge(12);
naruto.setTeacher(teacher);
//输出本体的地址
System.out.println("本体地址: "+naruto);
//输出影分身的地址
System.out.println("本体老师地址: "+naruto.getTeacher());
System.out.println("-----影-------分------身--------");
//获取影分身
DeepStudent shadowNaruto = (DeepStudent) naruto.clone();
//输出影分身的信息->直接复制了本体
System.out.println("影分身名字: "+shadowNaruto.getName());
//输出影分身老师的信息->直接复制了本体
System.out.println("影分身老师: "+shadowNaruto.getTeacher().getName());
//输出影分身的地址
System.out.println("影分身地址: "+shadowNaruto);
//输出影分身老师的地址
System.out.println("影分身老师地址: "+shadowNaruto.getTeacher());
System.out.println("-------------换老师-------------");
// 修改本体老师的信息
naruto.getTeacher().setName("卡卡西");
System.out.println("鸣人的老师是: " + naruto.getTeacher().getName());
System.out.println("鸣人的影分身老师是: " + shadowNaruto.getTeacher().getName());
}
}
运行结果:
从运行结果我们看到,不仅本体和影分身的地址不同,本体的老师地址和影分身的老师地址也不同了。说明了两个老师指向的是两个不同的对象,所以当我们对本体的老师进行修改的时候,并不会影响影分身的老师。
=========================================================================
然后我们来讨论一下原型模式:
原型模式的精髓就是复制,所以隐形模式一般常用于以下情况:
- 类初始化消耗资源较多
- new 产生的一个对象需要非常繁琐的过程(数据准备、访问权限等)
- 构造函数比较复杂
- 循环体中生产大量对象
也就是利用已有的一个原型对象,快速地生成和原型对象一样的实例。类似于模板生成。
这种形式涉及到三个角色:
- 客户角色:客户提出创建对象的请求。
- 抽象原型角色:这是一个抽象角色,通常由一个Java接口或Java抽象类实现,这个类可能会继承Cloneable接口。
- 具体原型角色:被复制的对象。此角色需要实现抽象的原型角色所要求的接口。
我们来实现一个试卷生成器:
我们有几道面试简答题用原型模式设计一个随机排序生成试卷的功能
=========================================================================
我们首先定义一个简答题的类:
age PrototypeDesignPattern.QuizBank.Questions;
/**
* @author Zeyu Wan
* @version 1.0.0
* @ClassName ShortAnswer.java
* @Description 简答题
* @createTime 2022年02月28日 16:00:00
*/
public class ShortAnswer {
/**
* 问题
*/
private String quiz;
/**
* 答案
*/
private String answer;
public ShortAnswer() {
}
public ShortAnswer(String quiz, String answer) {
this.quiz = quiz;
this.answer = answer;
}
public String getAnswer() {
return answer;
}
public String getQuiz() {
return quiz;
}
public void setAnswer(String answer) {
this.answer = answer;
}
public void setQuiz(String quiz) {
this.quiz = quiz;
}
}
抽象原型角色:就是考虑一个试卷的模板,这个模板是可以被复制的。
package PrototypeDesignPattern.QuizBank;
import PrototypeDesignPattern.QuizBank.Questions.ShortAnswer;
import java.util.ArrayList;
import java.util.Collections;
/**
* @author Zeyu Wan
* @version 1.0.0
* @ClassName QuizBankPrototype.java
* @Description 试卷生成模板类
* @createTime 2022年02月28日 16:10:00
*/
public class QuizBankPrototype implements Cloneable{
/**
* 考生
*/
private String name;
/**
* 考号
*/
private Integer id;
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setId(Integer id) {
this.id = id;
}
public Integer getId() {
return id;
}
private ArrayList<ShortAnswer> shortAnswerList = new ArrayList<ShortAnswer>();
public QuizBankPrototype append(ShortAnswer shortAnswer){
shortAnswerList.add(shortAnswer);
return this;
}
@Override
public Object clone() throws CloneNotSupportedException {
QuizBankPrototype quizBankPrototype = (QuizBankPrototype) super.clone();
quizBankPrototype.shortAnswerList = (ArrayList<ShortAnswer>) shortAnswerList.clone();
// 题目乱序
Collections.shuffle(quizBankPrototype.shortAnswerList);
return quizBankPrototype;
}
@Override
public String toString() {
StringBuilder detail = new StringBuilder("考生:" + name + "\r\n" +
"考号:" + id + "\r\n" +
"--------------------------------------------\r\n");
detail.append("题目:" + "\r\n");
for (int idx = 0; idx < shortAnswerList.size(); idx++) {
detail.append("第").append(idx + 1).append("题:").append(shortAnswerList.get(idx).getQuiz()).append("\r\n\n");
//detail.append("答案:").append(shortAnswerList.get(idx).getAnswer()).append("\r\n\n");
}
return detail.toString();
}
}
具体原型角色:就是设计的一个题库,来实现题目的插入,实际可以使用数据库,但是简单起见就直接插入了
package PrototypeDesignPattern.QuizBank;
import PrototypeDesignPattern.QuizBank.Questions.ShortAnswer;
/**
* @author Zeyu Wan
* @version 1.0.0
* @ClassName QuizBank.java
* @Description 添加题目
* @createTime 2022年02月28日 16:10:00
*/
public class QuizBank {
private QuizBankPrototype quizBank = new QuizBankPrototype();
public QuizBank(){
quizBank.append(new ShortAnswer("String s = “a” +“b” + “c” + “d”;这条语句创建了几个对象", "创建了一个对象,因为相对于字符串常量相加的表达式,编译器会在编译期间进行优化,直接将其编译成常量相加的结果。"))
.append(new ShortAnswer("String s; 创建几个对象", "没有创建对象"))
.append(new ShortAnswer("String a = “abc”; String b = “abc”; 创建了几个对象", "创建了一个对象,只是在第一条语句中创建了一个对象,a和b都指向相同的对象\"abc\",引用不是对象"))
.append(new ShortAnswer("讲一讲面向对象四大特性", "抽象,封装,继承,多态"));
}
public String createPaper(String name, Integer id) throws CloneNotSupportedException {
QuizBankPrototype questionBankClone = (QuizBankPrototype) quizBank.clone();
questionBankClone.setId(id);
questionBankClone.setName(name);
return questionBankClone.toString();
}
}
测试一下:
package PrototypeDesignPattern.QuizBank;
/**
* @author Zeyu Wan
* @version 1.0.0
* @ClassName QuizTest.java
* @Description 试卷生成器测试类
* @createTime 2022年02月28日 16:46:00
*/
public class QuizTest {
public static void main(String[] args) throws CloneNotSupportedException {
QuizBank quizbank = new QuizBank();
System.out.println(quizbank.createPaper("张三",1));
System.out.println(quizbank.createPaper("李四",2));
System.out.println(quizbank.createPaper("王五",3));
}
}
我们可以看到,我们只需要输入考生姓名和考号,我们的试卷生成器就可以随机顺序的生成对应的题目。
优点:
- 将产品的创建过程封装起来,客户端不需要了解产品的具体创建流程。
- 利用Java的clone方法来创建对象肯定要比使用new来创建对象快很多,尤其是那些很复杂的对象的时候。
- 可以在不修改其他代码的情况下添加新的产品,符合“开-闭”原则。
缺点:
- 原型模式的最大缺点就是每一个类必须都有一个clone方法,如果这个类的组成不太复杂的话还比较好,如果类的组成很复杂的话,如果想实现深度复制就非常困难了。