原型模式说白了就是对象的克隆,我们经常说为深拷贝、深克隆、浅拷贝、浅克隆等。原型模式也是属于六个创建型模式之一,其余五个:
- 简单工厂模式
- 工厂方法模式
- 抽象工厂模式
- 单例模式
- 建造者模式
这里需要注意的是,原型模式所涉及的就是对象的拷贝,在 Java 中,对象的拷贝有深拷贝、浅拷贝的概念。与深拷贝相对应有一个浅拷贝的概念,浅拷贝指的是「被赋值对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用都仍然指向原来的对象。」而深拷贝指的是被复制的对象无论是基本数据类型变量还是对象里面的引用,均与原型对象不一致,是全新的对象,在内存中也拥有新的地址,对深拷贝的对象的任何修改不会对原型对象产生影响。
原型模式的定义
用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
原型模式的结构及其相关角色
- Prototype(抽象原型类):它是声明克隆方法的接口,是所有具体原型类的公共父类,可以是抽象类也可以是接口,甚至还可以是具体实现类。在 Java 中顶层 Object 对象就声明了 clone() 方法。
- ConcretePrototype(具体原型类):它实现在抽象原型类中声明的克隆方法,在克隆方法中返回自己的一个克隆对象。
- Client(客户端类):让一个原型对象克隆自身从而创建一个新的对象,在客户类中只需要直接实例化或通过工厂方法等方式创建一个原型对象,再通过调用该对象的克隆方法即可得到多个相同的对象。由于客户类针对抽象原型类Prototype编程,因此用户可以根据需要选择具体原型类,系统具有较好的可扩展性,增加或更换具体原型类都很方便。
Java 中的深拷贝(深克隆)与浅拷贝(浅克隆)
掌握了对象的拷贝,也就是掌握了原型模式。所以这一标题栏下面我们着重讲解一下 Java 中的关于 Java 对象复制的一些相关知识。
Java 中的浅克隆
在浅克隆中,如果原型对象的成员变量是基本数据类型(包括 String),基本数据类型会复制一份给克隆对象,但如果原型对象的成员变量是引用,即原型对象中存在这对其他对象实例的引用,那么浅克隆会将引用对象的地址复制一份给克隆对象,也就是说原型对象引用的对象实例和克隆对象引用的对象实例其实都指向相同的内存地址。简单概括起来起来,浅拷贝就是复制自己本身和基本数据类型的成员变量,但是对于引用类型的成员变量并没有进行对象的「真正复制」操作。
在 Java 中,浅拷贝是通过重写顶层 Object 类的 clone() 方法实现,并且原型对象要实现 Cloneable 接口,clone() 方法底层使用的是 Arrays.copyof() 方法来进行,这也是一个浅拷贝的方法。举一个最简单的 Teacher 类有对 Student 类引用的代码例子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
package learn.design.patterns.prototype; /** * 原型设计模式之学生类 * * @ClassName: Student * @author: Glorze * @since: 2020/8/12 22:51 */ public class Student { private String name; private Integer age; public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
package learn.design.patterns.prototype; /** * 原型设计模式之教师类 * * Java 中浅拷贝代码示例 * * @ClassName: Teacher * @author: Glorze * @since: 2020/8/12 22:55 */ public class Teacher implements Cloneable { private String name; private Student student; public String getName() { return name; } public void setName(String name) { this.name = name; } public Student getStudent() { return student; } public void setStudent(Student student) { this.student = student; } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } public static void main(String[] args) throws CloneNotSupportedException { Teacher t1, t2; t1 = new Teacher(); Student student = new Student(); student.setName("高老四博客"); student.setAge(27); t1.setName("Glorze"); t1.setStudent(student); t2 = (Teacher) t1.clone(); System.out.println("t1 和 t2 是否相同:" + (t1 == t2)); System.out.println("原型对象和克隆对象的 Student 实例是否相同:" + (t1.getStudent() == t2.getStudent())); } } |
如你所见,Teacher 对象是成功克隆了,但是 Student 对象实例指向的同一内存地址。
Java 中的深克隆
了解了浅克隆,那么我们自然就知道深克隆是嘎哈的了,深克隆就是将上面的 Student 实例也要复制出新的一份内存地址,与原型对象引用的 Student 实例不一样。
在 clone() 方法中创建新的原型引用实例
将上面 Teacher 类中的 clone() 方法改写成如下:
1 2 3 4 5 6 7 8 |
@Override protected Object clone() throws CloneNotSupportedException { Teacher teacher = new Teacher(); teacher.setName(name); teacher.setStudent(new Student()); return teacher; // return super.clone(); } |
这样就可以实现引用对象实例的新地址复制。运行结果如下:
序列化实现 Java 中的深拷贝
关于序列化的相关知识点可以参考文末的相关文章阅读之序列化的讲解。
在原型对象构造函数中进行深克隆
这个理解起来也比较简单,我们直接看代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
package learn.design.patterns.prototype; /** * 原型设计模式之教师类 * * Java 中浅拷贝代码示例 * * @ClassName: Teacher * @author: Glorze * @since: 2020/8/12 22:55 */ public class Teacher implements Cloneable { private String name; private Student student; public Teacher() { } public Teacher(String name, Student student) { this.name = name; this.student = new Student(); } public String getName() { return name; } public void setName(String name) { this.name = name; } public Student getStudent() { return student; } public void setStudent(Student student) { this.student = student; } @Override protected Object clone() throws CloneNotSupportedException { Teacher teacher = new Teacher(name, student); return teacher; } public static void main(String[] args) throws CloneNotSupportedException { Teacher t1, t2; t1 = new Teacher(); Student student = new Student(); student.setName("高老四博客"); student.setAge(27); t1.setName("Glorze"); t1.setStudent(student); t2 = (Teacher) t1.clone(); System.out.println("t1 和 t2 是否相同:" + (t1 == t2)); System.out.println("原型对象和克隆对象的 Student 实例是否相同:" + (t1.getStudent() == t2.getStudent())); } } |
从以上代码,说白了就是把该引用对象复制的事放到原型对象的构造函数中去实现,结果依然是两个 Student 对象不相等。
原型模式的优缺点以及适用场景
原型模式的优点
原型模式的优势其实就是简化对象的创建,如果一个原始对象的构造相对比较复杂,那么使用原型模式可以快速的对已有复杂对象进行复制使用。另外对象的复制是通过 clone() 方法来实现,所以相比较于工厂模式,原型模式提供了简化的对象 创建结构
原型模式的缺点
- 违反「开放-封闭原则」,每一个原型类都要提供一个克隆方法,所以,当业务出现变动需要修改相关克隆逻辑的时候,就需要修改原型类。
- 如果原型对象中存在着引用的多层链式调用,那么对于深拷贝会加重 clone() 方法的实现逻辑。比如说 Teacher 类引用了 Grade 类,Grade 类里面又引用了 Student 类。多层这样的对象引用要求在克隆方法中均需要设置对应的深克隆对象新实例,代码不仅繁琐,并且不易维护。
原型模式适用场景
- 类初始化消耗资源比较多,构造函数比较复杂。
- 使用 new 生成一个对象需要非常繁琐的过程(数据准备、访问权限)
- 在循环体中产生大量对象
原型模式在 Spring 中的使用
在 Spring 中,原型模式体现在 Spring 的作用域(scope)中,先来回顾一下 Spring 中六种原型作用域:
- singleton(单例作用域):默认作用域,不管一个 bean 被注入多少次都是同一个实例,每个容器中只有一个 Bean 实例,由 BeanFactory 自身来维护。
- prototype(原型作用域):每次注入或者通过 Spring 应用上下文获取的时候,都会创建一个新的 bean 实例。这里就是原型模式的体现。
- request(请求作用域):在 Web 应用中,为每个请求创建一个 bean 实例。请求完成以后,Bean 会失效并且垃圾回收器回收。
- Session(会话作用域):在 Web 应用中,为每个会话创建一个 bean 实例。会话过期后,Bean 也会随之消失。
- global-session(全局会话作用域):web 基于 porlet 应用的作用域,类似于 Servlet 中的 Session。
- 自定义作用域:通过实现 Scope 接口实现自定义作用域。
我们简单来看一下源码,Spring 的作用域体现在 Bean 的实例化过程中,也就是 AbstractBeanFactory 的 doGetBean() 方法中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType, @Nullable final Object[] args, boolean typeCheckOnly) throws BeansException { // 省略部分代码 try { final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName); // 创建实例 if (mbd.isSingleton()) { // 单例作用域,对创建的 Bean 进行缓存,下次就不创建了。 sharedInstance = getSingleton(beanName, () -> { try { return createBean(beanName, mbd, args); } catch (BeansException ex) { // 创建失败就从缓存中删除 destroySingleton(beanName); throw ex; } }); bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd); } else if (mbd.isPrototype()) { // 原型作用域,每次都创建一个新的实例 Object prototypeInstance = null; try { beforePrototypeCreation(beanName); prototypeInstance = createBean(beanName, mbd, args); } finally { afterPrototypeCreation(beanName); } bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd); } // 其他作用域 else { String scopeName = mbd.getScope(); final Scope scope = this.scopes.get(scopeName); if (scope == null) { throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'"); } try { Object scopedInstance = scope.get(beanName, () -> { beforePrototypeCreation(beanName); try { return createBean(beanName, mbd, args); } finally { afterPrototypeCreation(beanName); } }); bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd); } catch (IllegalStateException ex) { throw new BeanCreationException(beanName, "Scope '" + scopeName + "' is not active for the current thread; consider " + "defining a scoped proxy for this bean if you intend to refer to it from a singleton", ex); } } } catch (BeansException ex) { cleanupAfterBeanCreationFailure(beanName); throw ex; } // 省略一部分代码 return (T) bean; } |
从以上我们看得出来单例和原型创建 Bean 的不同之处是,单例创建完之后 Spring 会记录他的实例状态并保存到缓存里面,下次创建会优先取缓存里的实例,而原型作用域创建的 Bean 则是直接创建实例化对象,并不会记录他的实例。
相关文章阅读
- 《Java十道由浅入深的面试题第一期(下) 详细解析》
- 《阿里巴巴Java开发规约第一章-OOP(面向对象编程)规约篇》
- 《浅析设计模式第一章之简单工厂模式》
- 《浅析设计模式第八章之工厂方法模式》
- 《浅析设计模式第二十一章之单例模式 值得收藏》
- 《Java中I/O输入输出流之对象序列化浅析》
- 《浅析设计模式第四章之开放-封闭原则》
更博不易,如果觉得文章对你有帮助并且有能力的老铁烦请捐赠盒烟钱,点我去赞助。或者扫描文章下面的微信/支付宝二维码打赏任意金额(点击「给你买杜蕾斯」),也可以加入本站封闭式交流论坛「DownHub」开启新世界的大门,老四这里抱拳谢谢诸位了。捐赠时请备注姓名或者昵称,因为您的署名会出现在赞赏列表页面,您的捐赠钱财也会被用于小站的服务器运维上面,再次抱拳感谢。