设计模式(三)——原型模式
一、引子
1、克隆人的问题
问题:有一个人叫张三,姓名:张三,年龄:18,身高:178。如何创建和张三属性完全相同的10个人呢?
代码示例:
1 public class Main { 2 3 public static void main(String[] args) { 4 Person p0 = new Person("张三", 18, 178); 5 6 Person p1 = new Person(p0.getName(), p0.getAge(), p0.getHeight()); 7 Person p2 = new Person(p0.getName(), p0.getAge(), p0.getHeight()); 8 Person p3 = new Person(p0.getName(), p0.getAge(), p0.getHeight()); 9 Person p4 = new Person(p0.getName(), p0.getAge(), p0.getHeight()); 10 Person p5 = new Person(p0.getName(), p0.getAge(), p0.getHeight()); 11 Person p6 = new Person(p0.getName(), p0.getAge(), p0.getHeight()); 12 Person p7 = new Person(p0.getName(), p0.getAge(), p0.getHeight()); 13 Person p8 = new Person(p0.getName(), p0.getAge(), p0.getHeight()); 14 Person p9 = new Person(p0.getName(), p0.getAge(), p0.getHeight()); 15 Person p10 = new Person(p0.getName(), p0.getAge(), p0.getHeight()); 16 17 System.out.println(JSON.toJSONString(p0)); 18 } 19 } 20 21 class Person { 22 private String name; 23 private int age; 24 private int height; 25 // getter & setter 26 // 有参数、无参构造器 27 }
这样创建的问题显而易见:如果Person类有100个属性呢?如果要创建和张三属性完全相同的100个人呢?
二、原型模式
1、介绍
原型模式(Prototype):用原型实例指定创建对象的种类,并且通过拷贝这些原型,创建新的对象,无需知道创建的细节。简单来说就是复制一个和已知对象一模一样的对象。
方式:实现 Cloneable 接口,复写 Object 类的 clone() 方法。该方法可以将一个 Java 对象复制一份,但调用该方法的类必须实现Cloneable接口,这是一个标志接口,标识该类能够复制且具有复制的能力。如果不实现 Cloneable 接口,直接调用 clone() 方法,会抛出 CloneNotSupportedException 异常。
源码示例:Object.clone()
1 protected native Object clone() throws CloneNotSupportedException;
这是一个本地方法,具体实现细节,不需要了解,由操作系统实现。只需要知道它的作用就是复制对象,产生一个新的对象。
2、解决克隆人的问题
代码示例:实现Cloneable接口,复写clone()方法。
1 public class Main { 2 3 public static void main(String[] args) { 4 Person p0 = new Person("张三", 18, 178); 5 6 final Person p1 = p0.clone(); 7 8 System.out.println(JSON.toJSONString(p1)); 9 System.out.println(p0 == p1); 10 } 11 } 12 13 class Person implements Cloneable { 14 private String name; 15 private int age; 16 private int height; 17 18 @Override 19 public Person clone() { 20 Person person = null; 21 try { 22 person = (Person) super.clone(); 23 } catch (CloneNotSupportedException e) { 24 e.printStackTrace(); 25 } 26 return person; 27 } 28 29 // getter & setter 30 // 有参数、无参构造器 31 } 32 33 // 结果 34 {"age":18,"height":178,"name":"张三"} 35 false
从结果可以看出,复制了一个和张三一模一样的人。
三、深拷贝与浅拷贝
1、介绍
在 Java 中有两种数据类型:基本类型和引用类型。
基本类型,也称为值类型,有八大基本数据类型:byte、short、int、long、float、double、char、boolean。注:String并不是基本数据类型。
引用类型,有数组、类、接口、枚举等。
浅拷贝:只复制基本数据类型,以及引用类型的引用。
深拷贝:复制基本数据类型,以及引用类型的引用,和引用对象的实例。
如图所示:克隆”张三”。
浅拷贝,只会克隆张三的基本属性,如姓名,年龄,身高等,不会克隆他的狗,他两共用这只狗。
深拷贝,除了克隆张三,还会克隆他的狗,他两分别独立拥有各自的狗。
2、浅拷贝
上述在解决克隆人的问题上,其实就是一种浅拷贝。接下来,假设张三拥有一只狗,叫”旺财”。
代码示例:
1 public class Main { 2 3 public static void main(String[] args) { 4 Dog dog = new Dog("旺财"); 5 Person p0 = new Person("张三", 18, 178, dog); 6 7 final Person p1 = p0.clone(); 8 9 // 修改p0 张三的狗的名字为 大黄 10 p0.getDog().setName("大黄"); 11 12 System.out.println(JSON.toJSONString(p1)); 13 } 14 } 15 16 class Person implements Cloneable { 17 private String name; 18 private int age; 19 private int height; 20 private Dog dog; // 引用类属性 21 22 @Override 23 public Person clone() { 24 Person person = null; 25 try { 26 person = (Person) super.clone(); 27 } catch (CloneNotSupportedException e) { 28 e.printStackTrace(); 29 } 30 return person; 31 } 32 // getter & setter 33 // 有参数、无参构造器 34 } 35 36 class Dog { 37 private String name; 38 // getter & setter 39 // 有参数、无参构造器 40 } 41 42 // 结果 43 {"age":18,"dog":{"name":"大黄"},"height":178,"name":"张三"}
问题:从结果可以看出,修改了张三的狗的名字,克隆张三的狗的名字也改变了。原因是他两指向的是同一只狗。也就是说对象经过克隆后,只是复制了其引用,其指向的还是同一块堆内存空间,当修改其中一个对象的属性,另一个也会跟着变化。
3、深拷贝
如果就想要,在克隆张三后,他两各自拥有自己独立的狗,怎么办呢?就要用到深拷贝。
实现方式一:让引用类属性也具备可拷贝性。
代码示例:让Dog同样具备可拷贝性
1 public class Main { 2 3 public static void main(String[] args) { 4 Dog dog = new Dog("旺财"); 5 Person p0 = new Person("张三", 18, 178, dog); 6 7 final Person p1 = p0.clone(); 8 9 // 修改p0 张三的狗的名字为 大黄 10 p0.getDog().setName("大黄"); 11 12 System.out.println(JSON.toJSONString(p1)); 13 System.out.println(JSON.toJSONString(p0)); 14 } 15 } 16 17 class Person implements Cloneable { 18 private String name; 19 private int age; 20 private int height; 21 private Dog dog; // 引用类属性 22 23 @Override 24 public Person clone() { 25 Person person = null; 26 try { 27 // 1.先克隆一个人 28 person = (Person) super.clone(); 29 30 // 2.再克隆一条狗,并让这个狗属于这个人. 31 person.dog = this.dog.clone(); 32 } catch (CloneNotSupportedException e) { 33 e.printStackTrace(); 34 } 35 return person; 36 } 37 // getter & setter 38 // 有参数、无参构造器 39 } 40 41 class Dog implements Cloneable { 42 private String name; 43 44 @Override 45 public Dog clone() { 46 Dog dog = null; 47 try { 48 dog = (Dog) super.clone(); 49 } catch (CloneNotSupportedException e) { 50 e.printStackTrace(); 51 } 52 return dog; 53 } 54 // getter & setter 55 // 有参数、无参构造器 56 } 57 58 // 结果 59 {"age":18,"dog":{"name":"旺财"},"height":178,"name":"张三"} 60 {"age":18,"dog":{"name":"大黄"},"height":178,"name":"张三"}
问题:从结果可以看出,他两各自拥有了独立的狗。这种方式,确实实现了对象的深拷贝。但是有一个问题,如果Dog类还有引用类属性,也需要同样让其具备可拷贝性,嵌套太深的话,很不利于代码的扩展性。
一般推荐使用方式二来实现。
实现方式二:序列化。对序列化不了解的,可以先看这篇,对象流。
代码示例:序列化
1 public class Main { 2 3 public static void main(String[] args) throws Exception { 4 Dog dog = new Dog("旺财"); 5 Person p0 = new Person("张三", 18, 178, dog); 6 // 序列化 7 ObjectOutputStream output = new ObjectOutputStream(new FileOutputStream("temp.dat")); 8 output.writeObject(p0); 9 10 // 反序列化 11 ObjectInputStream input = new ObjectInputStream(new FileInputStream("temp.dat")); 12 final Person p1 = (Person) input.readObject(); 13 14 // 修改p0 张三的狗的名字为 大黄 15 p0.getDog().setName("大黄"); 16 17 System.out.println(JSON.toJSONString(p1)); 18 System.out.println(JSON.toJSONString(p0)); 19 } 20 } 21 22 class Person implements Serializable { 23 private static final long serialVersionUID = 1L; 24 private String name; 25 private int age; 26 private int height; 27 private Dog dog; // 引用类属性 28 // getter & setter 29 // 有参数、无参构造器 30 } 31 32 class Dog implements Serializable { 33 private static final long serialVersionUID = 1L; 34 private String name; 35 // getter & setter 36 // 有参数、无参构造器 37 }
注意:因为使用了序列化,引用类属性,要想序列化,也需要具备可序列化。
代码示例:优化,可抽取为一个工具类
1 public class CloneUtils { 2 @SuppressWarnings("unchecked") 3 public static <T extends Serializable> T deepClone(T obj) { 4 T cloneObj = null; 5 try { 6 // 写入字节流 7 ByteArrayOutputStream out = new ByteArrayOutputStream(); 8 ObjectOutputStream obs = new ObjectOutputStream(out); 9 10 obs.writeObject(obj); 11 obs.close(); 12 13 // 分配内存,写入原始对象,生成新对象 14 ByteArrayInputStream ios = new ByteArrayInputStream(out.toByteArray()); 15 ObjectInputStream ois = new ObjectInputStream(ios); 16 17 cloneObj = (T) ois.readObject(); 18 ois.close(); 19 } catch (Exception e) { 20 e.printStackTrace(); 21 } 22 return cloneObj; 23 } 24 }
4、优点
性能高:原型模式是在内存二进制流的拷贝,要比直接new一个对象性能好很多,特别是要在一个循环体内产生大量的对象时,原型模式可以很好地体现其优点。
避免构造函数的约束:这既是它的优点也是缺点,直接在内存中拷贝,构造函数是不会执行的,优点就是减少了约束, 缺点也是减少了约束,需要在实际应用时考虑使用。
四、原型模型在框架中的应用