👉本文章全部文字纯原创,若是须要转载,请注明转载出处,谢谢!😘java
你们好,我是高冷就是范儿,很高兴又和你们见面了。😊今天咱们继续设计模式的探索之路。前几篇的内容有小伙伴尚未阅读过的,能够阅读一下。git
前文回顾
👉让设计模式飞一下子|单例模式
👉让设计模式飞一下子|工厂模式github
今天咱们接下来要聊的是原型模式。编程
❓何为原型?设计模式
维基百科上给出的概念:原型是独创的模型,表明同一类型的人物、物件、或观念。ide
以个人理解能力解释一下,就是说,它是一种类型的独创对象。在面向对象编程中,所谓的类型就是指类,也就是说,它是这个类的一个源实例。post
我仍是坚持前面几篇一向的风格,在深刻了解该模式以前,先来思考一下,这个模式它出现的缘由以及存在的意义是什么?性能
首先,这个模式也是属于建立型模式,也是用来建立对象。仍是回到以前反复说过的一个问题,就是咱们建立对象为何必定要使用原型模式呢?学习
像以前咱们学过的单例模式是由于须要控制对象个数必须是单个。工厂模式是须要将对象建立和使用解耦,使得能够在不须要知道建立细节而使用一个对象,那今天要学习的原型模式它用在建立对象上又是出于什么缘由呢?测试
举个简单例子
好比某一个公司有A和B两个产品线,如今假设须要在每个产品销售出去以前作一次检查,检查标准是,假如该产品的重量超过10kg,就从新生产一个新的。如今但愿将全部产品的检查逻辑用同一个通用方法实现,而且后续增长新产品后能够方便扩展,怎么实现这个需求?
你可能会以下实现(伪代码),
public class ProductCheck {
public void check(Product product) {
if (product.weight > 10) {
//若是该产品是A产品就建立一个新的A对象。
//若是该产品是B产品就建立一个新的对象。
}
}
}
复制代码
可是如今出问题了,写不下去发现没有?为何?
由于如今检查的这个对象重量超过10kg了,因此须要建立一个新的对象,但问题这个时候我并不知道传入的product
对象是什么类型啊?是A类型?仍是B类型?这个在你编译时期你是不知道的,天然这代码你就无法写下去了......
那该怎么解决这个问题呢?
有聪明的程序猿说了,这还不简单吗?直接在check()
方法中加个if else
判断一下不就行了吗?因而代码优化成以下,
public class ProductCheck {
public void check(Product product) {
if (product.weight > 10) {
if (product instanceof PA) {
//若是该产品是A产品就建立一个新的A对象。
} else if (product instanceof PB) {
//若是该产品是B产品就建立一个新的对象。
}
}
}
}
复制代码
其中PA
和PB
分别是Product
接口的子类,表示A产品和B产品。上面代码看上去貌似确实没啥问题,经过对传入的product
类型判断从而建立不一样类型的对象,很正常嘛?
❓可是这样写有个啥问题?
上面需求是这个通用方法须要知足,后续增长新产品后能够方便扩展。如今假设这个公司新增了一种C产品,也须要使用这个检查方法怎么办?这个时候你就必需要修改check()
方法的代码,增长else if (product instanceof PC)
的逻辑,还记得开闭原则吗?这显然违反了开闭原则,因此这个方案不可取。若是看过上篇工厂模式的同窗可能想起点什么?这个有点相似工厂模式里面的简单工厂模式嘛?
引发这个问题的本质在于哪里?
没错,就是由于check()
跟具体的产品类耦合了。
当时是怎么解决开闭原则的问题的?
没错是经过工厂方法模式解决的,因而优化后代码以下:
public class ProductCheck {
public void check(ProductFactory factory) {
Product product = factory.createProduct();
if (product.weight > 10) {
Product product2 = factory.createProduct();
}
}
}
复制代码
其中A产品和B产品各会有ProductFactory
的实现类,这样当新增产品时就不会出现开闭原则的问题了。没错,这个问题用工厂方法模式彻底能够解决,没问题。可是今天呢,咱们将要聊的原型模式也可能解决这个问题。这个时候确定会有人问了,既然工厂模式已经能够解决这一问题,那为何还要你的原型模式呢?
这个问题我会留到后面讲,如今先让咱们看一下原型模式是怎么解决这个问题的?
原型模式的原理是这样的,原型模式要求,每个对象须要定义一个克隆本身的方法。什么意思?好比一个A对象,他须要提供一个方法,调用这个方法将会返回一个本身的副本。通常来讲,会给全部须要克隆本身的对象提供一个公共的接口,这个接口里面会提供一个克隆自身的方法,以下,
interface CloneableObj {
Object cloneSelf();
}
复制代码
而后让全部须要克隆本身的类去实现该接口,天然会须要实现cloneSelf()
方法,这个方法内部就是克隆本身的逻辑实现。那如何实现克隆呢?
最傻瓜的办法,直接先new一个本身对象的实例,而后把本身实例中的数据取出来,设置到新的对象实例中去,不就能够完成实例的复制了嘛?这样这个cloneSelf()
方法返回的就是一个跟自身如出一辙的对象了。如下是代码实现:
class PA extends Product implements CloneableObj{
@Override
public Object cloneSelf() {
PA a = new PA();
a.weight = weight;
return a;
}
}
复制代码
这个时候假设你须要在工程其余代码中须要经过克隆方式快速获得一个PA对象,就能够经过调用原型PA对象(假设是a)a.cloneSelf()
轻松快速的获得一个PA
对象了。
没错,这个就是最本质的原型模式。其实说的简单一点,所谓的原型模式,就是复制(或克隆)模式,就是经过当前对象(原型对象)返回一个跟当前对象彻底相同的对象,包括其中的属性值。
这也是原型模式跟直接new的一个区别,咱们知道new生成的对象的属性值都是默认的,而经过原型模式返回的对象是将属性值一同复制。
其实,原型模式并不强制要求克隆生成的对象和原型对象彻底相同,并且也没有规定具体采用的克隆技术,这个能够由程序本身实现。只是在大部分实际应用场景中,用原型模式生成的对象都是和原型对象彻底相同或者相近。
其实,上面这个例子是为了更好的理解原型模式的本质,为了提升克隆效率,JDK
已经设计了关于对象克隆的功能。在Object
类中有一个clone()
方法,该方法就能够轻松的实现对对象自己进行克隆。上面例子中底层仍是采用new
的方式建立对象,可是Object.clone()
底层是直接对二进制数据流操做,所以效率会比直接new的方式高得多(看到后面,其实这句话说的不严谨)。不过要使用Object.clone()
来对自身对象克隆有个限制,就是该对象所对应的类必需要实现java.lang.Cloneable
接口,不然会抛出CloneNotSupportedException
异常。另外,通常须要被克隆的类都须要重写Object.clone()
,而且将访问修饰符改成public
,以方便在其余类中使用。因而代码实现以下:
public class ProductCheck {
public void check(Product product) throws CloneNotSupportedException {
if (product.weight > 10) {
Object o = product.clone();
}
}
}
复制代码
有人又会有疑问了,这样作相比较前面的工厂模式有啥优点?工厂方法模式也彻底能够实现相同的需求啊?
原型模式和工厂方法模式一个共同的优势是,他们均可以在不知道具体的类型状况之下,建立出某类型对象。好比上面例子中的Product
,这只是一个抽象接口,其下会有不少的子类,具体建立哪一种类型的子类对象取决于运行时期。原型模式是经过克隆自身的方式实现的,而工厂方法模式是经过不一样子类的工厂类实现的。
可是原型模式相比于工厂方法模式的优点在于,工厂方法模式底层仍是采用new
的方式建立对象,而且须要手动的为属性赋值,效率较差。而经过Object.clone()
实现的原型模式直接是操做二进制流实现,并且克隆生成的对象是已经赋好值了。所以效率要高得多。
那么,经过
new
的方式建立对象和调用clone()
方式建立对象,效率相差多少?
下面给出一个简单的测试例子:
public class A implements Cloneable {
private String a = "a";
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
public A() {}
}
public class Demo {
public static void main(String[] args) throws CloneNotSupportedException {
A a = new A();
long s1 = System.nanoTime();
for (int i = 0; i < 10000000; i++) {
new A();
}
long e1 = System.nanoTime();
System.out.println("new cost total: " + TimeUnit.NANOSECONDS.toMillis(e1 - s1));
long s2 = System.nanoTime();
for (int i = 0; i < 10000000; i++) {
a.clone();
}
long e2 = System.nanoTime();
System.out.println("clone cost total: " + TimeUnit.NANOSECONDS.toMillis(e2 - s2));
}
}
复制代码
上面输出的结果以下:
相信看到这个结果的朋友确定会大吃一惊⁉️震惊!怎么clone
的速度比new
还慢了这么多倍......😱和以前的认知截然不同了。
那结果然的是这样吗❓
咱们作一下小改动,其它代码都不作修改,但这一次咱们在new
所须要的构造器中加入一些耗时操做,以下:
public class A implements Cloneable {
//其他代码和上面的例子同样,省略,惟一区别在于加入下面的代码
public A() {
for (int i = 0; i < 1000; i++)
a += "a";
}
}
复制代码
为了节省测试时间,咱们把Demo
中的循环次数减小到10000就好,以下:
public class Demo {
public static void main(String[] args) throws CloneNotSupportedException {
A a = new A();
long s1 = System.nanoTime();
for (int i = 0; i < 10000; i++) {
new A();
}
long e1 = System.nanoTime();
System.out.println("new cost total: " + TimeUnit.NANOSECONDS.toMillis(e1 - s1));
long s2 = System.nanoTime();
for (int i = 0; i < 10000; i++) {
a.clone();
}
long e2 = System.nanoTime();
System.out.println("clone cost total: " + TimeUnit.NANOSECONDS.toMillis(e2 - s2));
}
}
复制代码
这一次测试结果以下:
这一次总算出现符合预期的结果了。
也就是说,对于自己建立过程不是很耗时的简单对象来讲,直接new的效率要比clone要高。可是若是是建立过程很复杂很耗时的对象,那使用clone的方式要比new的方式效率高得多。这也是clone()方法的意义所在。
也就是说,对于建立耗时复杂的对象,用原型模式能够大大提升建立对象的效率。到这里估计不少人应该能想到,既然这样,把这二者结合一下不就能够弥补工厂方法模式的缺陷了吗?
没错,传统的工厂方法模式中,各子类的工厂类建立对象的方法,好比上面的factory.createProduct()
底层仍是采用new
的方式,若是改为克隆方式就能够大大提升建立对象的效率了。思路比较简单,具体代码这边就不演示了。
另外,在原型模式中还会涉及到一个浅克隆和深克隆的问题,怎么理解呢?我举一个简单的例子,
//如下代码所有省略setter、getter、toString
public class A {
private int a;
}
public class B implements Cloneable{
private int b;
private A a;
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class ShallowClone {
public static void main(String[] args) throws CloneNotSupportedException {
A a = new A();
a.setA(1);
B b = new B();
b.setA(a);
b.setB(2);
B b2 = (B)b.clone(); ❶
System.out.println("b-->" + b);
System.out.println("b2-->" + b2);
b2.setB(3); ❷
System.out.println("b-->" + b);
System.out.println("b2-->" + b2);
b2.getA().setA(10);❸
System.out.println("b-->" + b);
System.out.println("b2-->" + b2);
}
}
//输出:
b-->B{b=2, a=A{a=1}}
b2-->B{b=2, a=A{a=1}}
b-->B{b=2, a=A{a=1}}
b2-->B{b=3, a=A{a=1}}
b-->B{b=2, a=A{a=10}}
b2-->B{b=3, a=A{a=10}}
复制代码
从上面这个代码分析可得出:
b.clone()
克隆获得的,因此这两个对象除了内存地址不一样,其他的内容都相同。int
类型。A
类型(引用类型)。由此,咱们能够得出一个结论,Object.clone()
实现的实际上是一种浅克隆模式。
在浅克隆模式下,克隆生成对象的基本数据类型(包括对应包装类)属性和String拷贝的是值,后续修改克隆对象的该属性值,并不会影响原来的对象里的值。但若是是引用类型属性拷贝的是引用,拷贝获得的对象和原来的对象的属性指向同一个对象。因此,后续修改其属性值,就会影响原来的对象里的对应的属性值。
而在有些场合下,咱们是但愿原型对象和新建立的对象不要相互干扰。这就是深克隆模式。
❓那怎么实现呢?
public class A implements Cloneable{
private int a;
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class B implements Cloneable{
private Integer b;
private A a;
@Override
public Object clone() throws CloneNotSupportedException {
B b = (B) super.clone(); ❶
A a = b.getA();
b.setA((A) a.clone());
return b;
}
}
public class DeepClone {
public static void main(String[] args) throws CloneNotSupportedException {
A a = new A();
a.setA(1);
B b = new B();
b.setA(a);
b.setB(2);
B b2 = (B)b.clone();
System.out.println("b-->" + b);
System.out.println("b2-->" + b2);
b2.setB(3);
System.out.println("b-->" + b);
System.out.println("b2-->" + b2);
b2.getA().setA(10);
System.out.println("b-->" + b);
System.out.println("b2-->" + b2);
}
}
//输出:
b-->B{b=2, a=A{a=1}}
b2-->B{b=2, a=A{a=1}}
b-->B{b=2, a=A{a=1}}
b2-->B{b=3, a=A{a=1}}
b-->B{b=2, a=A{a=1}}
b2-->B{b=3, a=A{a=10}}
复制代码
经过上面代码执行结果咱们不难看出,这个时候不管是修改b2中的a属性(引用类型)仍是b属性(基本类型),都不会影响到原型对象中的值了。
❓那这个是怎么实现的呢?
深克隆模式实现的关键在于❶行处,在B
对象经过调用clone()
复制本身的同时,将a属性(引用类型)也clone
了一份,而且赋值给生成的b2对象。
深克隆原理就是在每个原型对象执行clone()
方法的时候,同时将该对象中每个引用类型的属性的内容也拷贝一份,并设置到新建立的对象中。假设,每个引用类型中又嵌套着其它的引用类型的属性,再重复上面操做,以此类推,递归执行下去......这中间只要有一个没有这样操做,深克隆就失败。
这也是原型模式一大缺点,在实现深克隆复制时,每一个原型的子类都必须实现clone()
的操做,尤为是包含多层嵌套引用类型的对象时,必需要递归的让全部相关对象都正确的实现克隆操做,十分繁琐易错。
那有没有更好的办法来实现深克隆呢?
固然有!😎
可使用序列化和反序列化的手段实现对象的深克隆!
public class A implements Serializable {
private int a;
}
public class B implements Serializable {
private Integer b;
private A a;
}
public class DeepClone2 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
A a = new A();
a.setA(1);
B b = new B();
b.setA(a);
b.setB(2);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(b);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
B b2 = (B) ois.readObject();
System.out.println("b-->" + b);
System.out.println("b2-->" + b2);
b2.setB(3);
System.out.println("b-->" + b);
System.out.println("b2-->" + b2);
b2.getA().setA(10);
System.out.println("b-->" + b);
System.out.println("b2-->" + b2);
}
}
//输出:
b-->B{b=2, a=A{a=1}}
b2-->B{b=2, a=A{a=1}}
b-->B{b=2, a=A{a=1}}
b2-->B{b=3, a=A{a=1}}
b-->B{b=2, a=A{a=1}}
b2-->B{b=3, a=A{a=10}}
复制代码
经过上面的代码不难看出,序列化和反序列化确实实现了深克隆,并且在实现方式上比以前用重写clone()
的方式要简单的多,惟一须要作的就是给须要克隆的对象以及引用类型实现Serializable
接口便可。
最后来作个总结,其实原型模式更适合叫作克隆模式,它的本质就在于经过必定技术手段生成一个自身的副本。这能够经过咱们在文章最开始那样手动new一个,也能够经过Object.clone()
,还能够经过序列化和反序列化实现。若是原型对象中存在引用类型的属性,根据是否同时克隆该属性能够分为深克隆模式和浅克隆模式。
在大部分场景下,咱们主要会使用Object.clone()
方法来实现克隆,根据上面对clone()方法执行性能测试结果,在建立大量复杂对象时,这个方法的建立效率要远高于new的方式。所以若是须要建立大量而且复杂对象时能够采用原型模式。
另外,原型模式能够像工厂方法模式同样,能够在事先不知道具体类型的前提下建立出对象,也就是基于接口建立对象,并且实现方式比工厂模式更高效简单。
好了,今天关于原型模式的技术分享就到此结束,下一篇我会继续分享另外一个设计模式——建造者模式,一块儿探讨设计模式的奥秘。我们不见不散。😊👏