什么是原型模式?

如果对象的创建成本比较大,而同一个类的不同对象之间差别不大,在这种情况下,我们可以利用对已有对象(原型)进行复制(拷贝)的方式来创建新对象,以达到节省创建时间的目的。这种基于原型来创建对象的方式就叫作原型设计模式(Prototype Design Pattern)。使用这种方式创建新对象,就无需再通过 new 实例化来创建对象了。这是因为 Object 类的 clone 方法是一个本地方法,它可以直接操作内存中的二进制流,所以性能相对 new 实例化来说会更好。

实际上,创建对象包含的申请内存、给成员变量赋值这一过程,本身并不会花费太多时间,或者说对于大部分业务系统来说,这点时间完全是可以忽略的。应用一个复杂的模式,只得到一点点的性能提升,这就是所谓的过度设计,得不偿失。但是,如果对象中的数据需要经过复杂的计算才能得到(比如排序、计算哈希值),或者需要从 RPC、网络、数据库、文件系统等非常慢速的 IO 中读取,这种情况下,我们就可以利用原型模式,从其他已有对象中直接拷贝得到,而不用每次在创建新对象的时候,都重复执行这些耗时的操作了。

原型模式的实现

我们先通过一个简单的例子来实现一个原型模式:

  1. public class Prototype implements Cloneable {
  2. public void show(){
  3. System.out.println("原型模式实现类");
  4. }
  5. @Override
  6. public Prototype clone(){
  7. Prototype prototype = null;
  8. try{
  9. prototype = (Prototype)super.clone();
  10. }catch(CloneNotSupportedException e){
  11. e.printStackTrace();
  12. }
  13. return prototype;
  14. }
  15. }
  16. public class Client {
  17. public static void main(String[] args){
  18. Prototype cp = new Prototype();
  19. for(int i=0; i< 10; i++){
  20. Prototype clonecp = (Prototype)cp.clone();
  21. clonecp.show();
  22. }
  23. }
  24. }

要实现一个原型类,需要具备三个条件:

  • 实现 Cloneable 接口:Cloneable 接口与序列化接口的作用类似,它只是告诉虚拟机可以安全地在实现了这个接口的类上使用 clone 方法。在 JVM 中,只有实现了 Cloneable 接口的类才可以被拷贝,否则会抛出 CloneNotSupportedException 异常。
  • 重写 Object 类中的 clone 方法:在 Java 中,所有类的父类都是 Object 类,而 Object 类中有一个 clone 方法,作用是返回对象的一个拷贝。
  • 在重写的 clone 方法中调用 super.clone():默认情况下,类不具备复制对象的能力,需要调用 super.clone() 来实现。

从上面我们可以看出,原型模式的主要特征就是使用 clone 方法复制一个对象。通常,有些人会误以为 Object a=new Object();Object b=a; 这种形式就是一种对象复制的过程,然而这种复制只是对象引用的复制,也就是 a 和 b 对象指向了同一个内存地址,如果 b 修改了,a 的值也就跟着被修改了。我们可以通过一个简单的例子来看看普通的对象复制问题:

  1. @Getter
  2. @Setter
  3. public class Student {
  4. private String name;
  5. }
  6. public class Test {
  7. public static void main(String args[]) {
  8. Student stu1 = new Student();
  9. stu1.setName("test1");
  10. Student stu2 = stu1;
  11. stu2.setName("test2");
  12. System.out.println("学生1:" + stu1.getName());
  13. System.out.println("学生2:" + stu2.getName());
  14. }
  15. }

实际上,程序输出的结果是:

  1. 学生1:test2
  2. 学生2:test2

通过 clone 方法复制的对象才是真正的对象复制,clone 方法复制的对象完全是一个独立的对象。刚刚讲过了,Object 类的 clone 方法是一个本地方法,它直接操作内存中的二进制流,特别是复制大对象时,性能的差别非常明显。我们可以用 clone 方法再实现一遍以上例子。

  1. @Getter
  2. @Setter
  3. public class Student implements Cloneable {
  4. private String name;
  5. @Override
  6. public Student clone() {
  7. Student student = null;
  8. try {
  9. student = (Student) super.clone();
  10. } catch (CloneNotSupportedException e) {
  11. e.printStackTrace();
  12. }
  13. return student;
  14. }
  15. }
  16. public class Test {
  17. public static void main(String args[]) {
  18. Student stu1 = new Student(); //创建学生1
  19. stu1.setName("test1");
  20. Student stu2 = stu1.clone(); //通过克隆创建学生2
  21. stu2.setName("test2");
  22. System.out.println("学生1:" + stu1.getName());
  23. System.out.println("学生2:" + stu2.getName());
  24. }
  25. }

这次,程序输出的结果是:

  1. 学生1:test1
  2. 学生2:test2

浅拷贝和深拷贝

学习原型模式,我们还需要了解两个概念:深拷贝(Deep Copy)和浅拷贝(Shallow Copy)。

浅拷贝和深拷贝的区别在于,浅拷贝只会复制对象中基本数据类型数据和引用对象的内存地址,不会递归地复制引用对象,以及引用对象的引用对象。相反,深拷贝不仅仅会复制指针,还会复制对象本身。浅拷贝得到的对象跟原始对象共享数据,而深拷贝得到的是一份完完全全独立的对象。所以,深拷贝比浅拷贝更加耗时、更加耗内存空间。如果要拷贝的对象是不可变对象,浅拷贝共享不可变对象是没问题的,但对于可变对象来说,浅拷贝得到的对象和原始对象会共享部分数据,就有可能出现数据被修改的风险,也就变得复杂多了。

在 Java 语言中,Object 类的 clone() 方法执行的就是浅拷贝。它只会拷贝对象中基本类型的数据(int、long)以及引用对象的内存地址,而不会递归地拷贝引用对象本身。所以,当我们在使用 clone() 方法实现对象的克隆时,就需要注意浅拷贝带来的问题。我们再通过一个例子来看看浅拷贝。

  1. @Getter
  2. @Setter
  3. public class Student implements Cloneable {
  4. private String name;
  5. private Teacher teacher;
  6. @Override
  7. public Student clone() {
  8. Student student = null;
  9. try {
  10. student = (Student) super.clone();
  11. } catch (CloneNotSupportedException e) {
  12. e.printStackTrace();
  13. }
  14. return student;
  15. }
  16. }
  17. @Getter
  18. @Setter
  19. public class Teacher implements Cloneable{
  20. private String name;
  21. @Override
  22. public Teacher clone() {
  23. Teacher teacher= null;
  24. try {
  25. teacher= (Teacher) super.clone();
  26. } catch (CloneNotSupportedException e) {
  27. e.printStackTrace();
  28. }
  29. return student;
  30. }
  31. }
  32. public class Test {
  33. public static void main(String args[]) {
  34. Teacher teacher = new Teacher (); //定义老师1
  35. teacher.setName("刘老师");
  36. Student stu1 = new Student(); //定义学生1
  37. stu1.setName("test1");
  38. stu1.setTeacher(teacher);
  39. Student stu2 = stu1.clone(); //定义学生2
  40. stu2.setName("test2");
  41. stu2.getTeacher().setName("王老师");//修改老师
  42. System.out.println("学生" + stu1.getName + "的老师是:" + stu1.getTeacher().getName);
  43. System.out.println("学生" + stu1.getName + "的老师是:" + stu2.getTeacher().getName);
  44. }
  45. }

运行结果:

  1. 学生test1的老师是:王老师
  2. 学生test2的老师是:王老师

可以看到:在我们给学生 2 修改老师时,学生 1 的老师也跟着被修改了。这就是浅拷贝带来的问题。

我们可以通过深拷贝来解决这种问题,其实深拷贝就是基于浅拷贝来递归实现具体的每个对象、对象的引用对象以及引用对象的引用对象……直到要拷贝的对象只包含基本数据类型,没有引用对象为止。具体代码如下:

  1. public Student clone() {
  2. Student student = null;
  3. try {
  4. student = (Student) super.clone();
  5. Teacher teacher = this.teacher.clone();//克隆teacher对象
  6. student.setTeacher(teacher);
  7. } catch (CloneNotSupportedException e) {
  8. e.printStackTrace();
  9. }
  10. return student;
  11. }

此外,还有一种方法进行深拷贝:先将对象序列化,然后再反序列化成新的对象。具体代码如下:

  1. public Object deepCopy(Object object) {
  2. ByteArrayOutputStream bo = new ByteArrayOutputStream();
  3. ObjectOutputStream oo = new ObjectOutputStream(bo);
  4. oo.writeObject(object);
  5. ByteArrayInputStream bi = new ByteArrayInputStream(bo.toByteArray());
  6. ObjectInputStream oi = new ObjectInputStream(bi);
  7. return oi.readObject();
  8. }