设计模式04----原型设计模式(prototype)
参考文献:https://www.cnblogs.com/lwbqqyumidi/p/3746821.html
首先对原型模式进行一个简单概念说明:通过一个已经存在的对象,复制出更多的具有与此对象具有相同类型的新的对象。
在理解Java原型模式之前,首先需要理解Java中的一个概念:复制/克隆。
一. java.lang.Object类中的clone方法
源码如下:
protected native Object clone() throws CloneNotSupportedException;
看,clone()方法又是一个被声明为native的方法,因此,我们知道了clone()方法并不是Java的原生方法,具体的实现是有C/C++完成的。clone英文翻译为"克隆",其目的是创建并返回此对象的一个副本。形象点理解,这有一辆科鲁兹,你看着不错,想要个一模一样的。你调用此方法即可像变魔术一样变出一辆一模一样的科鲁兹出来。配置一样,长相一样。但从此刻起,原来的那辆科鲁兹如果进行了新的装饰,与你克隆出来的这辆科鲁兹没有任何关系了。你克隆出来的对象变不变完全在于你对克隆出来的科鲁兹有没有进行过什么操作了。Java术语表述为:clone函数返回的是一个引用,指向的是新的clone出来的对象,此对象与原对象分别占用不同的堆空间。
明白了clone的含义后,接下来看看如果调用clone()函数对象进行此克隆操作。
首先看一下下面的这个例子:
1 package com.corn.objectsummary; 2 3 import com.corn.Person; 4 5 public class ObjectTest { 6 7 public static void main(String[] args) { 8 9 Object o1 = new Object(); 10 // The method clone() from the type Object is not visible 11 Object clone = o1.clone(); 12 } 13 14 }
例子很简单,在main()方法中,new一个Oject对象后,想直接调用此对象的clone方法克隆一个对象,但是出现错误提示:"The method clone() from the type Object is not visible"
why? 根据提示,第一反应是ObjectTest类中定义的Oject对象无法访问其clone()方法。回到Object类中clone()方法的定义,可以看到其被声明为protected,估计问题就在这上面了,protected修饰的属性或方法表示:在同一个包内或者不同包的子类可以访问。显然,Object类与ObjectTest类在不同的包中,但是ObjectTest继承自Object,是Object类的子类,于是,现在却出现子类中通过Object引用不能访问protected方法,原因在于对"不同包中的子类可以访问"没有正确理解。
"不同包中的子类可以访问",是指当两个类不在同一个包中的时候,继承自父类的子类内部且主调(调用者)为子类的引用时才能访问父类用protected修饰的成员(属性/方法)。 在子类内部,主调为父类的引用时并不能访问此protected修饰的成员。!(super关键字除外)
于是,上例改成如下形式,我们发现,可以正常编译:
1 package com.corn.objectsummary; 2 3 4 public class ObjectTest { 5 6 public static void main(String[] args) { 7 ObjectTest ot1 = new ObjectTest(); 8 9 try { 10 ObjectTest ot2 = (ObjectTest) ot1.clone(); 11 } catch (CloneNotSupportedException e) { 12 // TODO Auto-generated catch block 13 e.printStackTrace(); 14 } 15 } 16 17 }
是的,因为此时的主调已经是子类的引用了。
上述代码在运行过程中会抛出"java.lang.CloneNotSupportedException",表明clone()方法并未正确执行完毕,问题的原因在与Java中的语法规定:
clone()的正确调用是需要实现Cloneable接口,如果没有实现Cloneable接口,并且子类直接调用Object类的clone()方法,则会抛出CloneNotSupportedException异常。
Cloneable接口仅是一个表示接口,接口本身不包含任何方法,用来指示Object.clone()可以合法的被子类引用所调用。
于是,上述代码改成如下形式,即可正确指定clone()方法以实现克隆。
1 package com.corn.objectsummary; 2 3 public class ObjectTest implements Cloneable { 4 5 public static void main(String[] args) { 6 7 ObjectTest ot1 = new ObjectTest(); 8 9 try { 10 ObjectTest ot2 = (ObjectTest) ot1.clone(); 11 System.out.println("ot2:" + ot2); 12 System.out.println(ot1.equals(ot2)); 13 System.out.println(ot1==ot2); 14 } catch (CloneNotSupportedException e) { 15 // TODO Auto-generated catch block 16 e.printStackTrace(); 17 } 18 } 19 20 } 21 22 23 24 ot2:com.test.b.ObjectTest@33909752 25 false 26 false
Java中的对象复制/克隆分为浅复制和深复制。
1. 浅复制
我们知道,一个类的定义中包括属性和方法。属性用于表示对象的状态,方法用于表示对象所具有的行为。其中,属性既可以是Java中基本数据类型,也可以是引用类型。Java中的浅复制通常使用clone()方式完成。
当进浅复制时,clone函数返回的是一个引用,指向的是新的clone出来的对象,此对象与原对象分别占用不同的堆空间。同时,复制出来的对象具有与原对象一致的状态。
此处对象一致的状态是指:复制出的对象与原对象中的属性值完全相等==。
下面以复制一本书为例:
(1)定义Book类和Author类:
1 class Author { 2 3 private String name; 4 private int age; 5 6 public String getName() { 7 return name; 8 } 9 10 public void setName(String name) { 11 this.name = name; 12 } 13 14 public int getAge() { 15 return age; 16 } 17 18 public void setAge(int age) { 19 this.age = age; 20 } 21 22 }
1 class Book implements Cloneable { 2 3 private String title; 4 private int pageNum; 5 private Author author; 6 7 public Book clone() { 8 Book book = null; 9 try { 10 book = (Book) super.clone(); 11 } catch (CloneNotSupportedException e) { 12 // TODO Auto-generated catch block 13 e.printStackTrace(); 14 } 15 return book; 16 } 17 18 public String getTitle() { 19 return title; 20 } 21 22 public void setTitle(String title) { 23 this.title = title; 24 } 25 26 public int getPageNum() { 27 return pageNum; 28 } 29 30 public void setPageNum(int pageNum) { 31 this.pageNum = pageNum; 32 } 33 34 public Author getAuthor() { 35 return author; 36 } 37 38 public void setAuthor(Author author) { 39 this.author = author; 40 } 41 42 }
(2)测试
1 package com.qqyumidi; 2 3 public class PrototypeTest { 4 5 public static void main(String[] args) { 6 Book book1 = new Book(); 7 Author author = new Author(); 8 author.setName("corn"); 9 author.setAge(100); 10 book1.setAuthor(author); 11 book1.setTitle("好记性不如烂博客"); 12 book1.setPageNum(230); 13 14 Book book2 = book1.clone(); 15 16 System.out.println(book1 == book2); // false 17 System.out.println(book1.getPageNum() == book2.getPageNum()); // true 18 System.out.println(book1.getTitle() == book2.getTitle()); // true 19 System.out.println(book1.getAuthor() == book2.getAuthor()); // true 20 21 } 22 }
由输出的结果可以验证说到的结论。由此我们发现:虽然复制出来的对象重新在堆上开辟了内存空间,但是,对象中各属性确保持相等。对于基本数据类型很好理解,但对于引用数据类型来说,则意味着此引用类型的属性所指向的对象本身是相同的, 并没有重新开辟内存空间存储。换句话说,引用类型的属性所指向的对象并没有复制。
由此,我们将其称之为浅复制。当复制后的对象的引用类型的属性所指向的对象也重新得以复制,此时,称之为深复制。
2. 深复制
Java中的深复制一般是通过对象的序列化和反序列化得以实现。序列化时,需要实现Serializable接口。
下面还是以Book为例,看下深复制的一般实现过程:
(1)定义Book类和Author类(注意:不仅Book类需要实现Serializable接口,Author同样也需要实现Serializable接口!!):
1 class Author implements Serializable{ 2 3 private String name; 4 private int age; 5 6 public String getName() { 7 return name; 8 } 9 10 public void setName(String name) { 11 this.name = name; 12 } 13 14 public int getAge() { 15 return age; 16 } 17 18 public void setAge(int age) { 19 this.age = age; 20 } 21 22 }
1 class Book implements Serializable { 2 3 private String title; 4 private int pageNum; 5 private Author author; 6 7 public Book deepClone() throws IOException, ClassNotFoundException{ 8 // 写入当前对象的二进制流 9 ByteArrayOutputStream bos = new ByteArrayOutputStream(); 10 ObjectOutputStream oos = new ObjectOutputStream(bos); 11 oos.writeObject(this); 12 13 // 读出二进制流产生的新对象 14 ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); 15 ObjectInputStream ois = new ObjectInputStream(bis); 16 return (Book) ois.readObject(); 17 } 18 19 public String getTitle() { 20 return title; 21 } 22 23 public void setTitle(String title) { 24 this.title = title; 25 } 26 27 public int getPageNum() { 28 return pageNum; 29 } 30 31 public void setPageNum(int pageNum) { 32 this.pageNum = pageNum; 33 } 34 35 public Author getAuthor() { 36 return author; 37 } 38 39 public void setAuthor(Author author) { 40 this.author = author; 41 } 42 43 }
(2)测试
1 public class PrototypeTest { 2 3 public static void main(String[] args) throws ClassNotFoundException, IOException { 4 Book book1 = new Book(); 5 Author author = new Author(); 6 author.setName("corn"); 7 author.setAge(100); 8 book1.setAuthor(author); 9 book1.setTitle("好记性不如烂博客"); 10 book1.setPageNum(230); 11 12 Book book2 = book1.deepClone(); 13 14 System.out.println(book1 == book2); // false 15 System.out.println(book1.getPageNum() == book2.getPageNum()); // true 16 System.out.println(book1.getTitle() == book2.getTitle()); // false 17 System.out.println(book1.getAuthor() == book2.getAuthor()); // false 18 19 } 20 }
从输出结果中可以看出,深复制不仅在堆内存上开辟了空间以存储复制出的对象,甚至连对象中的引用类型的属性所指向的对象也得以复制,重新开辟了堆空间存储。
3.克隆破坏单例模式
如果我们克隆的目标对象是单例对象,就意味着会破坏单例模式。为了避免,可以采取如下方法:
1)单例类不实现Cloneable接口;
2)重写clone(),在clone()方法中返回单例对象。
二. 原型设计模式
其实上面的两个例子都是使用了原型设计模式。它有两个要点,一个是原型,另外一个是创建原型的复制对象的地方
使用场景:
1.如果创建新对象成本较大(比如数据准备、访问权限等),我们可以利用已有的对象进行复制来获得。
2.类初始化消耗资源较多;
3.构造函数比较复杂;
4.在循环体中产生大量对象;
在spring中,原型模式使用的非常广泛,比如scope="prototype",Json.parseObject也是一种原型模式。