创建型设计模式 之 原型模式
Posted jiangwj
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了创建型设计模式 之 原型模式相关的知识,希望对你有一定的参考价值。
同为创建型模式的原型模式与单例模式是密不可分的,这也是最常用的设计模式之一。
原型模式是一种非常简单的设计模式。这里除了基本介绍和演示,还详细介绍了Java中原型模式的本质。
一、介绍
同样,先来看一下《研磨设计模式》的定义——用原型实例指定创建对象的种类,并通过拷贝这些原型创建新的对象。
原型模式的本质——克隆生成对象。
那么原型模式是什么意思呢?说白了就是克隆自身。我们知道Java中没有引用这个概念,Java用变量名代表引用。像 Apple a = new Apple();我们知道,想要操作这个Apple对象,那么就是去操作"a"这个变量名,实质就是去操作“a”这个引用所指向的内存地址上的Apple对象。通常而言,赋值操作符“=”的本质就是将“=”右边的地址赋值给左边的引用。如果我们希望创建一个跟这个对象a一样的Apple对象,同时在操作这个新对象的时候,对象a无任何影响。Java新手可能下意识觉得使用Apple b = a; 很明显a和b指向的是同一片内存空间。
原型模式就是为了解决这样的对象复制的问题。
Java中的原型模式实现起来其实很简单,在对象的接口中添加一个复制自身的抽象方法,然后对象实现这个方法,复制自身即可。使用的时候直接调用接口方法即可。
二、我的实现
1、我们有一个水果接口,如下:
1 public interface Fruit { 2 3 public double getPrice(); 4 5 public void setPrice(double price); 6 7 //克隆接口 8 public Fruit cloneFruit(); 9 }
2、一个简单实现类:
1 public class Apple implements Fruit,Cloneable { 2 3 // 价格 4 private double price; 5 // 平均尺寸 6 private double avgSize; 7 // 产地 8 private String productionArea; 9 10 public Apple(double price, double avgSize, String productionArea) { 11 super(); 12 this.price = price; 13 this.avgSize = avgSize; 14 this.productionArea = productionArea; 15 } 16 17 public double getAvgSize() { 18 return avgSize; 19 } 20 21 public void setAvgSize(double avgSize) { 22 this.avgSize = avgSize; 23 } 24 25 public String getProductionArea() { 26 return productionArea; 27 } 28 29 public void setProductionArea(String productionArea) { 30 this.productionArea = productionArea; 31 } 32 33 @Override 34 public void setPrice(double price) { 35 // TODO Auto-generated method stub 36 this.price = price; 37 } 38 39 @Override 40 public double getPrice() { 41 // TODO Auto-generated method stub 42 return price; 43 } 44 45 @Override 46 public String toString() { 47 return "Apple [avgSize=" + avgSize + ", price=" + price 48 + ", productionArea=" + productionArea + "]"; 49 } 50 51 //克隆自身的实现 52 @Override 53 public Fruit cloneFruit() 54 { 55 return new Apple(price, avgSize, productionArea); 56 } 57 58 }
3、然后就可以测试了,如下:
1 package prototype.myPrototype; 2 3 public class Client { 4 5 public static void main(String[] args) 6 { 7 Fruit fruit = new Apple(1,2,"红富士"); 8 System.out.println(fruit); 9 10 //根据原对象克隆 11 Fruit cloneFruit = fruit.cloneFruit(); 12 System.out.println(cloneFruit); 13 System.out.println("两个水果是同一个吗?"+(fruit==cloneFruit)); 14 15 } 16 }
4、结果如下:
Apple [avgSize=2.0, price=1.0, productionArea=红富士] Apple [avgSize=2.0, price=1.0, productionArea=红富士] 两个水果是同一个吗?false
如上,简单的实现了原型模式。也实现了接口与实现分离的克隆。在完全不知道对象类型的情况下完成了复制。
三、Java的原型模式(Object的clone()方法)
(1)、介绍
Object类有一个clone()方法,这是java为实现原型模式准备的。
要实现Java的克隆方法,要满足三个条件:
1、调用对象实现了cloneable接口
2、调用对象重写了public Object clone();方法。
3、重写clone()时,调用super.clone();
我们来看一下clone方法的原型
protected native java.lang.Object clone() throws java.lang.CloneNotSupportedException
我们可以看到,这个方法是protected访问控制,同时是一个本地方法。也就是说,任何Object的子类都可以调用这个方法,但是Object对象自身不能调用这个方法。
这个方法执行的时候,是使用RTTI(run-time type identification)的机制,动态得找到目前正在调用clone方法的那个reference,根据它的大小申请内存空间,然后进行bitwise的复制,将该对象的内存空间完全复制到新的空间中去,从而达到shallowcopy的目的。(这句话摘自百度知道)
(2)、问题:
1、为什么调用对象要实现cloneable接口?
因为clone方法执行的时候,会判断当前对象是否实现了Cloneable这个标识接口,如果没有实现,就抛出CloneNotSupportedException异常。
2、为什么调用对象要重写public Object clone();方法,为什么要调用super.clone()?
如果调用对象不重写public Object clone()方法,那么clone()方法都不会显示出来。调用clone()方法,直接编译错误。可是clone()方法不是protected的吗?
我们来看一下clone();方法的API:
By convention, the returned object should be obtained by calling super.clone. If a class and all of its superclasses (except Object) obey this convention,
it will be the case that x.clone().getClass() == x.getClass().
--按照惯例,返回的对象应该通过调用 super.clone 获得。如果一个类及其所有的超类(Object 除外)都遵守此约定,则 x.clone().getClass() == x.getClass()
因为只有重写clone()方法,我们才能通过使用super.clone(),才能真正调用Object类的本地方法clone();
从这里我们可以看到,clone()方法是很特殊的。不重写,会编译错误,应该涉及到编译器的优化。
(3)、实现:
我们来Java的原型模式简单改一下上面的示例:
1、将Fruit接口中的克隆功能去掉,这里不列出了。
2、Apple类实现Fruit接口,也实现Cloneable接口,并重写clone()方法,如下:
1 public class Apple implements Fruit, Cloneable { 2 3 // 价格 4 private double price; 5 // 平均尺寸 6 private double avgSize; 7 // 产地 8 private String productionArea; 9 10 public Apple(double price, double avgSize, String productionArea) 11 { 12 super(); 13 this.price = price; 14 this.avgSize = avgSize; 15 this.productionArea = productionArea; 16 } 17 18 public double getAvgSize() 19 { 20 return avgSize; 21 } 22 23 public void setAvgSize(double avgSize) 24 { 25 this.avgSize = avgSize; 26 } 27 28 public String getProductionArea() 29 { 30 return productionArea; 31 } 32 33 public void setProductionArea(String productionArea) 34 { 35 this.productionArea = productionArea; 36 } 37 38 @Override 39 public void setPrice(double price) 40 { 41 // TODO Auto-generated method stub 42 this.price = price; 43 } 44 45 @Override 46 public double getPrice() 47 { 48 // TODO Auto-generated method stub 49 return price; 50 } 51 52 // 重写了Object类的clone()方法 53 @Override 54 public Object clone() 55 { 56 // TODO Auto-generated method stub 57 Object cloneApple = null; 58 // 直接调用父类的克隆方法即可 59 try 60 { 61 cloneApple = super.clone(); 62 } catch (CloneNotSupportedException e) 63 { 64 e.printStackTrace(); 65 } 66 return cloneApple; 67 } 68 69 @Override 70 public String toString() 71 { 72 return "Apple [avgSize=" + avgSize + ", price=" + price + ", productionArea=" + productionArea + "]"; 73 } 74 75 }
3、测试一下:
1 public class Client { 2 3 static Fruit fruit = new Apple(90, 5, "新疆"); 4 5 public static void main(String[] args) { 6 System.out.println("Fruit:" + fruit); 7 Fruit newFruit = (Fruit) ((Apple)fruit).clone(); 8 System.out.println("new Fruit : " + newFruit); 9 System.out.println("前后是否为同一对象?" + (fruit == newFruit)); 10 } 11 }
4、结果:
Fruit:Apple [avgSize=5.0, price=90.0, productionArea=新疆] new Fruit : Apple [avgSize=5.0, price=90.0, productionArea=新疆] 前后是否为同一对象?false
个人认为,方便而言,还不如自己手动实现原型模式。
由于Java的原型使用RTTI的机制,速度块,若需要克隆重量级或复杂的对象时适合使用。不过要注意的是,Java的原型模式属于浅度克隆。下面我们会讲如何使用Java的原型模式实现深度克隆。
这里再补充一点:
我们来看,static Fruit fruit = new Apple(90, 5, "新疆");
对于对象fruit,它的编译时类型是Fruit,运行时类型是Apple,所以fruit对象只能显式调用Fruit接口所有的方法。而这其中包含了Object类除了clone()方法之外的所有方法。Java中,接口和类是一个并列的概念
我们可以认为接口是一种特殊的类,它默认实现了Object中除了clone()之外的像toString()、getClass等一系列Object方法。
四、深度克隆和浅度克隆
克隆分为深度克隆和浅度克隆。假设我们现在有一个对象,这个对象有一个引用属性是另一个对象。我们知道,引用属性的本质就是一个内存地址。按照上文的复制,我们会将复制对象的引用属性也一同复制,这样一来,复制对象和被复制对象的该属性都指向同一内存地址了。这就违背了我们克隆的初衷。而这个就是浅度克隆。
相对的,深度克隆就是克隆之后,复制对象和被复制对象是完全不想干的。即对于对象的引用属性,我们重新克隆一次。
下面是Java克隆、深度克隆的简单示例:
1、首先是一个包含刚才Fruit类的Businessman对象:
@Data public class Businessman implements Cloneable { private String name; private Fruit fruit; public Businessman(String name, Fruit fruit) { super(); this.name = name; this.fruit = fruit; } //克隆方法 public Object clone(){ //先克隆一下引用变量 Fruit f = (Fruit) ((Apple)fruit).clone(); Businessman man = new Businessman(name,f); return man; } }
2、测试一下:
1 package prototype; 2 3 public class Client { 4 5 public static void main(String[] args) { 6 //水果类 7 Fruit fruit = new Apple(90, 5, "新疆"); 8 //商人 9 Businessman man1 = new Businessman("张三",fruit); 10 11 System.out.println("man1:" + man1); 12 //克隆 13 Businessman man2 = (Businessman) man1.clone(); 14 15 System.out.println("man2 : " + man2); 16 System.out.println("man1和man2前后是否为同一对象?" + (man1 == man2)); 17 System.out.println("两者的属性呢?"+(man1.getFruit()==man2.getFruit())); 18 } 19 }
3、结果如下:
man1:Businessman [name=张三, fruit=Apple [avgSize=5.0, price=90.0, productionArea=新疆]] man2 : Businessman [name=张三, fruit=Apple [avgSize=5.0, price=90.0, productionArea=新疆]] man1和man2前后是否为同一对象?false 两者的属性呢?false
以上是关于创建型设计模式 之 原型模式的主要内容,如果未能解决你的问题,请参考以下文章