文章目录
原型模式、建造者模式、代理模式
六、Prototype Pattern原型模式
原型模式的定义
原型模式(Prototype Pattern)是指原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象
调用者不需要知道任何创建细节,不调用构造函数
属于创建型模式
原型模式是可以通过拷贝原有对象创建新的对象,既然是通过拷贝创建新的对象,那我们通常是如何做拷贝的呢,是不是想下面这种硬编码的方式呢?
package org.example.pattren.prototype.demo;
public class ExamPaper {
// 测试:随便弄几个成员变量
private String examinationPaperId;
private String leavTime;
private String id;
//我们很多人复制对象都会这样写吧。
public ExamPaper copy(){
ExamPaper examPaper = new ExamPaper();
examPaper.setExaminationPaperId(this.getExaminationPaperId());
examPaper.setLeavTime(this.leavTime);
examPaper.setId(this.getId());
return examPaper;
}
public String getExaminationPaperId() {
return examinationPaperId;
}
public void setExaminationPaperId(String examinationPaperId) {
this.examinationPaperId = examinationPaperId;
}
public String getLeavTime() {
return leavTime;
}
public void setLeavTime(String leavTime) {
this.leavTime = leavTime;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
如上所示,我们通常new一个新的空对象,通过get和set来达到拷贝的效果。但有些同学可能想到利用循环来实现拷贝,我们看下面的代码:
package org.example.pattren.prototype.demo;
import java.lang.reflect.Field;
public class BeanUtils {
//比较常见复制对象的方式
public static Object copy(Object protorype){
Class clazz = protorype.getClass();
Object returnValue = null;
try {
returnValue = clazz.newInstance();
// 获得所有字段
for (Field field : clazz.getDeclaredFields()) {
field.setAccessible(true); //开放权限
field.set(returnValue,field.get(protorype));
}
} catch (Exception e) {
e.printStackTrace();
}
return returnValue;
}
}
我们看到上面两种方法其本质没有什么区别,都是用get和set方法来进行拷贝,只是一个运用循环减少了部分代码。
我们在来看一下原型模式的一种常规写法
IPrototype接口:定义clone方法
public interface IPrototype<T> {
T clone(); //你写什么都可以,约定俗成一般都用clone(克隆)
}
package org.example.pattren.prototype.general;
public class ConcretePrototype implements IPrototype{
private int age;
private String name;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public ConcretePrototype clone() {
ConcretePrototype concretePrototype = new ConcretePrototype();
concretePrototype.setAge(this.age);
concretePrototype.setName(this.name);
return concretePrototype;
}
@Override
public String toString() {
return "ConcretePrototype{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
测试类
package org.example.pattren.prototype.general;
public class Client {
public static void main(String[] args) {
//创建原型对象
ConcretePrototype prototype = new ConcretePrototype();
prototype.setName("CodeQs");
prototype.setAge(18);
System.out.println(prototype);
//拷贝原型对象
ConcretePrototype cloneType = prototype.clone();
System.out.println(cloneType);
}
}
代码不难,就是把前面copy的内容变成接口中clone方法的实现
原型模式的适用场景
1、类初始化消耗资源较多。
2、new产生的一个对象需要非常繁琐的过程(数据准备、访问权限等)。
3、构造函数比较复杂。
4、循环体重产生大量对象时。
原型模式一般分为两种,一个是浅克隆,一个是深克隆,我们一一讲解。
1、原型模式-浅克隆
首先我们来看浅克隆,我们在上面通用写法的基础上做一些扩展。来看代码
我们不再需要使用IPrototype接口,而是用JDK自带的Cloneable接口:
package org.example.pattren.prototype.shallow;
import lombok.Data;
import java.util.List;
@Data //有这个参数JVM在编译时,会自动生成get和set方法。这个方法如何配置,在度娘搜@Data有很多文章可参考
public class ConcretePrototype implements Cloneable {
private int age;
private String name;
@Override
public ConcretePrototype clone() {
try {
return (ConcretePrototype) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
return null;
}
}
@Override
public String toString() {
return "ConcretePrototype{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
既然我们用了Cloneable接口,那我们也一定要看一下他的源码。
public interface Cloneable {
}
没错接口内什么都没有,但我们注意到他接口前的说明。
* @author unascribed
* @see java.lang.CloneNotSupportedException
* @see java.lang.Object#clone()
* @since JDK1.0
本质上调用的是Object类的clone方法,进到源码看看
protected native Object clone() throws CloneNotSupportedException;
clone是个native方法,我们不继续深究了,我们只需要知道他能做到克隆的效果即可。我们重写的clone的方法中调用了super.clone()也就是调用了Object中的clone方法。
这是一个测试类_
package org.example.pattren.prototype.shallow;
import java.util.ArrayList;
public class Client {
public static void main(String[] args) {
//创建原型对象
ConcretePrototype prototype = new ConcretePrototype();
prototype.setName("CodeQs");
prototype.setAge(18);
//拷贝原型对象
ConcretePrototype cloneType = prototype.clone();
System.out.println("原型对象" + prototype); //没有做更改
System.out.println("克隆后对象" + cloneType);
System.out.println(prototype == cloneType); // 地址?并不一致
}
}
结果
原型对象ConcretePrototype{age=18, name='CodeQs'}
克隆后对象ConcretePrototype{age=18, name='CodeQs'}
false
这么看上面的程序完美的克隆了新的对象,但我们的类的成员都是基本数据类型和String类型,如果换成引用类型可不可以呢?既然提出这个问题,我们就要验证一下。
package org.example.pattren.prototype.shallow;
import lombok.Data;
import java.util.List;
@Data
public class ConcretePrototype implements Cloneable {
private int age;
private String name;
private List<String> hobbies;
@Override
public ConcretePrototype clone() {
try {
return (ConcretePrototype) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
return null;
}
}
@Override
public String toString() {
return "ConcretePrototype{" +
"age=" + age +
", name='" + name + '\'' +
", hobbies=" + hobbies +
'}';
}
}
加入一个List类型的hobbies来进行测试
package org.example.pattren.prototype.shallow;
import java.util.ArrayList;
import java.util.List;
public class Client {
public static void main(String[] args) {
//创建原型对象
ConcretePrototype prototype = new ConcretePrototype();
prototype.setName("CodeQs");
prototype.setAge(18);
List<String> hobbies = new ArrayList<>();
hobbies.add("书法");
hobbies.add("唱歌");
prototype.setHobbies(hobbies);
System.out.println("原型对象" + prototype); //没有做更改
//拷贝原型对象
ConcretePrototype cloneType = prototype.clone ();
cloneType.getHobbies().add("技术流");
System.out.println("原型对象" + prototype); //没有做更改
System.out.println("克隆后对象" + cloneType);
System.out.println(prototype == cloneType); // 地址?并不一致
}
}
结果
原型对象ConcretePrototype{age=18, name='CodeQs', hobbies=[书法, 唱歌]}
克隆后对象ConcretePrototype{age=18, name='CodeQs', hobbies=[书法, 唱歌, 技术流]}
false
貌似也没什么问题,对新生成的对象修改也没有影响原有对象。地址也不一样,那是不是就是克隆成功了。
我们不如将代码的位置稍作修改
System.out.println("原型对象" + prototype); //没有做更改
System.out.println("克隆后对象" + cloneType);
System.out.println(prototype == cloneType); // 地址?并不一致
再测试一下
原型对象ConcretePrototype{age=18, name='CodeQs', hobbies=[书法, 唱歌, 技术流]}
克隆后对象ConcretePrototype{age=18, name='CodeQs', hobbies=[书法, 唱歌, 技术流]}
false
好像有些不对,对克隆后的对象进行修改但是原型对象也同时被修改了。这和我们的想法不太一样。问题出在了引用类型hobbies这里,我们打印一下看看。测试类中加入如下代码:
System.out.println("原型对象的爱好" + prototype.getHobbies());
System.out.println("克隆对象的爱好" + cloneType.getHobbies());
System.out.println(prototype.getHobbies() == cloneType.getHobbies());
结果
原型对象ConcretePrototype{age=18, name='CodeQs', hobbies=[书法, 唱歌, 技术流]}
克隆后对象ConcretePrototype{age=18, name='CodeQs', hobbies=[书法, 唱歌, 技术流]}
false
原型对象的爱好[书法, 唱歌, 技术流]
克隆对象的爱好[书法, 唱歌, 技术流]
true
问题出现了,克隆前后的hobbies地址相同,也就是说浅克隆只是将地址复制给克隆后的对象,并没有将内容进行复制。
这也就是浅克隆存在问题,对引用类型只会克隆地址,内容不会修改。就会出现问题。
为了解决这个问题,我们引入了深克隆。
2、原型模式-深克隆
在学习浅克隆的时候我们发现一个问题,就是浅克隆无法对引用类型进行克隆,只会复制其引用地址。这与我们的需求有些出入,所以这一节我们来解决这个问题。
我们回忆一下我们在单例模式中说过序列化(反序列化时会产生新的对象)可以破坏单例,当时我们重写了readResolve方法来解决破坏单例的问题,如果不重写方法那序列化破坏单例的模式我们就可以利用起来实现深克隆。
package org.example.pattren.prototype.deep;
import lombok.Data;
import java.io.*;
import java.util.List;
@Data
public class ConcretePrototype implements Cloneable,Serializable {
private int age;
private String name;
private List<String> hobbies;
public ConcretePrototype deepClone(){
try {
//序列化
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(this);
//反序列化
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return (ConcretePrototype) ois.readObject();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
@Override
public String toString() {
return "ConcretePrototype{" +
"age=" + age +
", name='" + name + '\'' +
", hobbies=" + hobbies +
'}';
}
}
我们写了一个deepClone方法来实现深克隆,序列化相关内容在这就不详细说明了。
把之前的测试类拉过来测试一下
package org.example.pattren.prototype.deep;
import java.util.ArrayList;
import java.util.List;
public class Client {
public static void main(String[] args) {
//创建原型对象
ConcretePrototype prototype = new ConcretePrototype();
prototype.setName("CodeQs");
prototype.setAge(18);
List<String> hobbies = new ArrayList<>();
hobbies.add("书法");
hobbies.add("唱歌");
prototype.setHobbies(hobbies);
//拷贝原型对象
ConcretePrototype cloneType = prototype.deepClone();
cloneType.getHobbies().add("技术流");
System.out.println("原型对象" + prototype); //没有做更改
System.out.println("克隆后对象" + cloneType);
System.out.println(prototype == cloneType); // 地址?并不一致
System.out.println("原型对象的爱好" + prototype.getHobbies());
System.out.println("克隆对象的爱好" + cloneType.getHobbies());
System.out.println(prototype.getHobbies() == cloneType.getHobbies());
}
}
结果:
原型对象ConcretePrototype{age=18, name='CodeQs', hobbies=[书法, 唱歌]}
克隆后对象ConcretePrototype{age=18, name='CodeQs', hobbies=[书法, 唱歌, 技术流]}
false
原型对象的爱好[书法, 唱歌]
克隆对象的爱好[书法, 唱歌, 技术流]
false
通过测试结果看,浅克隆存在的问题被解决了,引用类型的内容也被克隆出来,不再是只复制地址了。
讲单例模式的时候我们说过,序列化会破坏单例。那么克隆会不会破坏单例呢?举个例子:我们以饿汉式单例为例
package org.example.pattren.prototype.singleton;
import lombok.Data;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
@Data
public class ConcretePrototype implements Cloneable {
private int age;
private String name;
private List<String> hobbies;
private static ConcretePrototype instance = new ConcretePrototype(); //new 一个对象
private ConcretePrototype(){} //私有化构造方法
public static ConcretePrototype getInstance(){ //提供一个全局访问点
return instance;
}
@Override
public ConcretePrototype clone() {
try {
return (ConcretePrototype) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
return null;
}
}
@Override
public String toString() {
return "ConcretePrototype{" +
"age=" + age +
", name='" + name + '\'' +
", hobbies=" + hobbies +
'}';
}
}
测试一下,结果:
原型对象ConcretePrototype{age=18, name='CodeQs', hobbies=[书法, 唱歌, 技术流]}
克隆后对象ConcretePrototype{age=18, name='CodeQs', hobbies=[书法, 唱歌, 技术流]}
false
原型对象的爱好[书法, 唱歌, 技术流]
克隆对象的爱好[书法, 唱歌, 技术流]
true
单例被破坏了,那我们应该如何解决呢?
第一种解决办法:单例模式不应该实现Cloneable接口
第二种解决方法:可以实现Cloneable但不能是之前的写法,需要做一些改进
public ConcretePrototype deepCloneHobbies(){
return instance;
}
可能有人会问,你这样做没有什么意义。确实是没意义,原型和单例本身就是冲突的,用单例就不要用原型,用原型就不要用单例!
3、原型模式在源码中的应用
我们看一下ArrayList和HashMap中的clone都是怎么实现的
ArrayList
public Object clone() {
try {
ArrayList<?> v = (ArrayList<?>) super.clone(); //创建一个新对象
v.elementData = Arrays.copyOf(elementData, size); //将原型对象的值分别付过来,深克隆
v.modCount = 0;
return v;
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
}
HashMap
@SuppressWarnings("unchecked")
@Override
public Object clone() {
HashMap<K,V> result;
try {
result = (HashMap<K,V>)super.clone(); //创建一个对象
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
result.reinitialize();
result.putMapEntries(this, false); //新对象赋值,还是深克隆
return result;
}
大家可以自己看一下源码,就不细说了
4、原型模式的优缺点
看一下优缺点吧
优点:性能优良,Java自带的 原型模式 是基于内存二进制流的拷贝,比直接new一个对象性能上提升了许多
可以使用申客隆方式保存对象的状态,使用原型模式将对象复制一份并将其状态保存起来,简化了创建过程
缺点:必须配备克隆(或者可拷贝)方法。
当对已有类进行改造的时候,需要修改代码,违反了开闭原则。
深拷贝、浅拷贝需要运用得当
七、Builder Pattern建造者模式
1、建造者模式的定义
建造者模式(Builder Pattern)是将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
特征:用户只需制定需要建造的类型就可以获得对象,建造过程及细节不需要了解。
属于创建型模式。
2、建造者模式的适用场景
适用于创建对象需要很多步骤,但是步骤逇顺序不一定固定。
如果一个对象有非常复杂的内部结构(很多属性)。
把复杂对象的创建和使用分离。
1、常规写法
我们来看一下建造者模式比较常规的一种写法
Product产品类:这里只演示一下写法,不再写很多的字段(@Data原型模式说明过,这里不再解释了)
package org.example.pattren.builder.general;
import lombok.Data;
@Data
public class Product {
private String name;
@Override
public String toString() {
return "Product{" +
"name='" + name + '\'' +
'}';
}
}
IBuilder接口:定义了build方法
package org.example.pattren.builder.general;
public interface IBuilder {
Product build();
}
ConcreteBuilder建造者类:实现IBuilder接口
package org.example.pattren.builder.general;
public class ConcreteBuilder implements IBuilder{
private Product product = new Product();
@Override
public Product build() {
return product;
}
}
Director调用类
package org.example.pattren.builder.general;
public class Director {
public static void main(String[] args) {
IBuilder builder = new ConcreteBuilder();
System.out.println(builder.build());
}
}
上面四部分展示了建造者模式的基本写法,没有添加多少字段,也没有把建造者模式的特点展示出来,在后续的部分会详细演示如何使用。
2、实际使用
现在我们把他利用起来,还是用Course课程类来举例
Course类:
package org.example.pattren.builder.simple;
import lombok.Data;
@Data //这个应该不用说了吧
public class Course {
private String name;
private String ppt;
private String video;
private String note;
private String homework;
}
MyBuilder接口:
package org.example.pattren.builder.simple;
public interface MyBuilder {
Course build();
}
CourseBuilder建造者类:
package org.example.pattren.builder.simple;
public class CourseBuilder implements MyBuilder{
private Course course = new Course();
public void addName(String name){ course.setName(name); }
public void addPpt(String ppt){ course.setPpt(ppt); }
public void addVideo(String video){ course.setVideo(video); }
public void addNote(String note){ course.setNote(note); }
public void addHomework(String homework){ course.setHomework(homework); }
@Override
public Course build() {
return course;
}
}
测试类Test:
package org.example.pattren.builder.simple;
public class Test {
public static void main(String[] args) {
CourseBuilder builder = new CourseBuilder();
//很少用
builder.addName("设计模式");
builder.addPpt("[PPT课件]");
builder.addVideo("录播视频");
System.out.println(builder.build());
}
}
但我们平时很少用这种写法,这每加一个属性,就要builder.显得比较累赘。我们就要在这种写法的基础上进行部分优化,省略builder.。我们来看下面这种优化的写法。
Course类:没有区别
package org.example.pattren.builder.chain;
import lombok.Data;
@Data
public class Course {
private String name;
private String ppt;
private String video;
private String note;
private String homework;
@Override
public String toString() {
return "Course{" +
"name='" + name + '\'' +
", ppt='" + ppt + '\'' +
", video='" + video + '\'' +
", note='" + note + '\'' +
", homework='" + homework + '\'' +
'}';
}
}
CourseBuilder类
package org.example.pattren.builder.chain;
public class CourseBuilder implements MyBuilder {
private Course course = new Course();
public CourseBuilder addName(String name){
course.setName(name);
return this;
}
public CourseBuilder addPpt(String ppt){
course.setPpt(ppt);
return this;
}
public CourseBuilder addVideo(String video){
course.setVideo(video);
return this;
}
public CourseBuilder addNote(String note){
course.setNote(note);
return this;
}
public CourseBuilder addHomework(String homework){
course.setHomework(homework);
return this;
}
@Override
public Course build() {
return course;
}
}
每个字段都给一个方法,并且方法的返回值都设置为this,也就是返回对象本身。这里先不说为什么,我们看测试类如何写?
测试类
package org.example.pattren.builder.chain;
public class Test {
public static void main(String[] args) {
CourseBuilder builder = new CourseBuilder()
.addName("设计模式")
.addPpt("[PPT课件]")
.addVideo("[录播视频]");
System.out.println(builder.build());
}
}
我们看到这个测试类的写法,大家可以理解建造者类返回this的原因了吗。返回的是this对象,也就是builder自己。所以.addName等同于builder.addName ; 后面就是builder.addPpt,builder.addVideo ;
看一下输出结果吧
Course{name='设计模式', ppt='[PPT课件]', video='[录播视频]', note='null', homework='null'}
3、源码中的应用
StringBuilder类为例
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
public StringBuilder append(StringBuffer sb) {
super.append(sb);
return this;
}
@Override
public StringBuilder append(CharSequence s) {
super.append(s);
return this;
}
@Override
public StringBuilder append(CharSequence s, int start, int end) {
super.append(s, start, end);
return this;
}
写法与我们写的差不多哈。都是return this; 就不再说了哈。
4、优缺点
优点:
封装性好,创建和使用分离
扩展性好,建造类之间独立、一定程度上解耦
缺点:
产生多余的Builder对象
产品内部发生变化,建造者都要修改,成本较大
创造者模式和工厂模式的区别
1、建造者模式更加注重方法的调用顺序,工厂模式注重于创建对象。
2、创建对象的力度不同,建造者模式创建复杂的对象,由各种复杂的部件组成,工厂模式创建出来的都一样。
3、关注点:工厂模式只需要把对象创建出来就可以了,而建造者模式中不仅要创建出这个对象,还要知道这个对象由哪些部件组成
4、建造者模式根据建造过程中的顺序不一样,最终的对象部件组成也不一样