设计模式 -- 原型模式 图解java对象克隆 引用拷贝、浅拷贝、深拷贝、序列化拷贝

发布时间:2022-02-28 20:00:01 作者:yexindonglai@163.com 阅读(544)

什么是原型模式

原型模式是一个创建型的模式。原型二字表明了该模式应该有一个样板实例,用户从这个样板对象中复制一个内部属性一致的对象,这个过程也就是我们称的“克隆”。被复制的实例就是我们所称的“原型”,这个原型是可定制的。原型模式多用于创建复杂的或者构造耗时的实例,因为这种情况下,复制一个已经存在的实例可使程序运行更高效。关键就是两个字:克隆

 

对象克隆简介

对象克隆,说白了,就是将已实例化的对象复制一个出来,有个别同学就要问了,字节new 出来或者用反射创建一个实例出来不就好了吗?  干嘛要克隆? 当然你可以new也可以反射,但是你new出来的对象都是空的,我们克隆是是将已有内容的对象复制一个一模一样的出来,比如我们看看下面这个复制对象的案例;

  1. // 实例化对象
  2. StudentInfo studentInfo = new StudentInfo();
  3. studentInfo.setName("张三");
  4. studentInfo.setAge(16);
  5. studentInfo.setHeight(149);
  6. studentInfo.setLoginName(zhangsan);
  7. studentInfo.setStatus(1);
  8. studentInfo.setAddress("广东省深圳市南山区深圳湾一号3栋28楼2802房");
  9. .......
  10. studentInfo.setN("N"); // 第N个对象
  11. // 复制对象
  12. StudentInfo copyInfo = new StudentInfo();
  13. copyInfo.setName(studentInfo.getName());
  14. copyInfo.setAge(studentInfo.getAge());
  15. copyInfo.setHeight(studentInfo.getHeight());
  16. copyInfo.setLoginName(studentInfo.getLoginName());
  17. copyInfo.setStatus(studentInfo.getStatus());
  18. copyInfo.setAddress(studentInfo.getAddress());
  19. ........
  20. copyInfo.setN(studentInfo.getN()); // 设置第N个对象

以上的复制对象方式是可行的,但是需要每个都重新赋值一遍,太麻烦啦,而且如果对象太多的话,你不得写好多代码,维护起来也麻烦得多,

 

原型模式使用场景

  1. 类初始化需要消化非常多的资源,这个资源包括数据、硬件资源等,通过原型拷贝避免这些消耗
  2.  通过new产生的一个对象需要非常繁琐的数据准备或者权限,这时可以使用原型模式。 
  3. 一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时,可以考虑使用原型模式拷贝多个对象供调用者使用,即保护性拷贝。

 

什么时候要用到对象拷贝

Spring默认是单例模式的,但如果在类上面加上  @Scope("prototype")  注解,就表明这个对象是原型模式,它的低层其实就是拷贝了一个容器中的对象实现的;

  1. @Service
  2. @Scope("prototype")
  3. public class StudentService {
  4. }

让我们看看java API里面一个用到了克隆对象的方法  java.util.Date:

  1. /**
  2. * Return a copy of this object.
  3. */
  4. public Object clone() {
  5. Date d = null;
  6. try {
  7. d = (Date)super.clone();
  8. if (cdate != null) {
  9. d.cdate = (BaseCalendar.Date) cdate.clone();
  10. }
  11. } catch (CloneNotSupportedException e) {} // Won't happen
  12. return d;
  13. }

 

爱学习的童鞋发问:“介绍完了对象拷贝,那如果需要实现对象拷贝的话,有哪些方式呢?”  ,首先,这是个好问题, 让我们把掌声送给这位童鞋,这可问到点子上了,对象拷贝啊一共有3种方式,分别为

  1. 浅拷贝
  2. 深拷贝
  3. 序列化深拷贝

接下来我们一个个地介绍它们的作用

 

拷贝引用 (不算拷贝,只是共用指针而已)

    有个聪明的同学马上就想出来了一个法子,并且站起来大声说:“脑师,我想到了,是不是这样的啊,(接着在电脑面前对着键盘一顿输出),先是画出一个流程图,然后敲了一段代码,这也太简单了吧, 我很聪明吧?嘿嘿”; 先看看这位童鞋画的图吧!

让我们在看看这位聪明的同学写的代码吧!

  1. public static void main(String[] args) {
  2. Student zhangsan = new Student(new StudentInfo("张三", 15));
  3. System.out.println("引用拷贝前张三的地址:"+zhangsan);
  4. Student copy = zhangsan; // 拷贝对象
  5. System.out.println("引用拷贝后张三的地址:"+copy);
  6. // 将名称改为李四
  7. System.out.println("将名称改为李四");
  8. zhangsan.getInfo().setName("李四");
  9. System.out.println("获取名称:"+copy.getInfo().getName());
  10. }

可是运行之后,结果是这样的:
 

  1. 引用拷贝前张三的地址:com.clone.Student@66d3c617
  2. 引用拷贝后张三的地址:com.clone.Student@66d3c617
  3. 将名称改为李四
  4. 获取名称:李四

这样也没错,算是实现了拷贝对象,但注意看后面的,我把名称改为李四, 复制的对象也跟着改了;原来啊,虽然用了2个对象,但是呢,我们可以看到打印的toString() 的内存地址是一样的,所以本质上,虽然看上去复制了一个对象出来,其实本质上它们共用了一个引用,引向同一块内存地址;改了其中一个,另一个也会跟着改;

 

 

1、浅拷贝

    当然,我们还有另一种方式可以实现克隆后的对象会使用不同的地址,就是用 clone 的方式,Object 是所有类的父类,这个类有个方法叫做clone() 方法, 这个方法就是用来克隆用的,使用这个方法后,会为我们拷贝一份基本类型,但是不拷贝引用类型,这2个引用类型也是指向同一块内存地址的;  所以,浅拷贝只会拷贝基本数据类型,不会拷贝引用类型,基本数据类型会分配一块新的内存地址来存储,引用类型用的是同一块内存地址

注意:使用clone() 方法必须先实现  Cloneable  接口,否则会报错;

我们看看代码是怎么实现的吧

  1. package com.clone;
  2. import java.util.ArrayList;
  3. /**
  4. * 浅拷贝
  5. */
  6. public class ShallowCopy {
  7. public static void main(String[] args) throws Exception {
  8. ArrayList<String> subjectList = new ArrayList<>();
  9. subjectList.add("数学");
  10. subjectList.add("语文");
  11. Student zhangsan = new Student("张三", 15, subjectList);
  12. Object clone = zhangsan.clone();
  13. System.out.println("浅拷贝前张三的地址:" + zhangsan);
  14. System.out.println("浅拷贝后张三的地址:" + clone);
  15. Student student = (Student) clone;
  16. // 将名称改为李四
  17. System.out.println("将浅拷贝后的名称改为李四,科目增加体育");
  18. student.setName("李四");
  19. student.getSubjectList().add("体育");
  20. System.out.println("获取浅拷贝修改后名称:" + student.getName());
  21. System.out.println("获取浅拷贝修改后科目:" + student.getSubjectList());
  22. System.out.println("获取浅拷贝前名称:" + zhangsan.getName());
  23. System.out.println("获取浅拷贝前科目:" + zhangsan.getSubjectList());
  24. }
  25. }
  26. // 学生类
  27. class Student implements Cloneable { // 实现Cloneable 接口
  28. // 名称
  29. private String name;
  30. // 年龄,基本数据类型
  31. private int age;
  32. // 科目列表 ,引用类型
  33. private ArrayList<String> subjectList;
  34. public Student(String name, int age, ArrayList<String> subjectList) {
  35. this.name = name;
  36. this.age = age;
  37. this.subjectList = subjectList;
  38. }
  39. public String getName() { return name; }
  40. public void setName(String name) { this.name = name; }
  41. public int getAge() { return age; }
  42. public void setAge(int age) { this.age = age; }
  43. public ArrayList<String> getSubjectList() { return subjectList; }
  44. public void setSubjectList(ArrayList<String> subjectList) { this.subjectList = subjectList; }
  45. @Override
  46. protected Object clone() throws CloneNotSupportedException {
  47. Object clone = super.clone(); // 浅拷贝
  48. Student student = (Student) clone;
  49. return student;
  50. }
  51. }

打印结果
 

  1. 浅拷贝前张三的地址:com.clone.Student@66d3c617
  2. 浅拷贝后张三的地址:com.clone.Student@63947c6b
  3. 将浅拷贝后的名称改为李四,科目增加体育
  4. 获取浅拷贝修改后名称:李四
  5. 获取浅拷贝修改后科目:[数学, 语文, 体育]
  6. 获取浅拷贝前名称:张三 (因为拷贝了一份新的基本数据类型,所以修改后不会影响原对象)
  7. 获取浅拷贝前科目:[数学, 语文, 体育] (引用对象未拷贝,修改后原对象的科目也跟着变化)

这下我们就可以看到,这里2个对象打印的内存地址已经不一样了,并且将张三改为李四,克隆对象也不受影响;

 

2、深拷贝

    在浅克隆中,对象的成员变量是基本数据类型(Integer 、Float、Char、Long 、Double、Short、Boolean、Byte、包括String ),但如果对象的成员变量也是一个对象,就需要把这个成员变量对象也克隆一份出来,如果不克隆对象类型的成员变量,那么它们本质上的数据还是指向同一块地址,所以我们既要克隆对象,也要克隆这个对象的成员变量;深拷贝会拷贝整个对象,会分配一块新的内存地址来存储数据

 

实现代码

  1. package com.clone;
  2. import java.util.ArrayList;
  3. /**
  4. * 深拷贝
  5. */
  6. public class DeepCopy {
  7. public static void main(String[] args) throws Exception {
  8. ArrayList<String> subjectList = new ArrayList<>();
  9. subjectList.add("数学");
  10. subjectList.add("语文");
  11. Student zhangsan = new Student("张三", 15, subjectList);
  12. Object clone = zhangsan.clone();
  13. System.out.println("深拷贝前张三的地址:" + zhangsan);
  14. System.out.println("深拷贝后张三的地址:" + clone);
  15. Student student = (Student) clone;
  16. // 将名称改为李四
  17. System.out.println("将深拷贝后的名称改为李四,科目增加体育");
  18. student.setName("李四");
  19. student.getSubjectList().add("体育");
  20. System.out.println("获取深拷贝修改后名称:" + student.getName());
  21. System.out.println("获取深拷贝修改后科目:" + student.getSubjectList());
  22. System.out.println("获取深拷贝前名称:" + zhangsan.getName());
  23. System.out.println("获取深拷贝前科目:" + zhangsan.getSubjectList());
  24. }
  25. }
  26. // 学生类
  27. class Student implements Cloneable { // 实现Cloneable 接口
  28. // 名称
  29. private String name;
  30. // 年龄,基本数据类型
  31. private int age;
  32. // 科目列表 ,引用类型
  33. private ArrayList<String> subjectList;
  34. public Student(String name, int age, ArrayList<String> subjectList) {
  35. this.name = name;
  36. this.age = age;
  37. this.subjectList = subjectList;
  38. }
  39. public String getName() { return name; }
  40. public void setName(String name) { this.name = name; }
  41. public int getAge() { return age; }
  42. public void setAge(int age) { this.age = age; }
  43. public ArrayList<String> getSubjectList() { return subjectList; }
  44. public void setSubjectList(ArrayList<String> subjectList) { this.subjectList = subjectList; }
  45. @Override
  46. protected Object clone() throws CloneNotSupportedException {
  47. Object clone = super.clone(); // 浅拷贝
  48. Student student = (Student) clone;
  49. student.subjectList = (ArrayList<String>) this.subjectList.clone(); // 深拷贝
  50. return student;
  51. }
  52. }

打印结果

  1. 深拷贝前张三的地址:com.clone.Student@66d3c617
  2. 深拷贝后张三的地址:com.clone.Student@63947c6b
  3. 将深拷贝后的名称改为李四,科目增加体育
  4. 获取深拷贝修改后名称:李四
  5. 获取深拷贝修改后科目:[数学, 语文, 体育]
  6. 获取深拷贝前名称:张三
  7. 获取深拷贝前科目:[数学, 语文]

 

 

3、序列化深拷贝(解决多层克隆问题)

     如果引用类型里面还包含很多引用类型,或者内层引用类型的类里面又包含引用类型,如果每个对象都去实现Cloneable 接口并重写clone方法的话,就会很麻烦。就像这种格式的对象:

这时我们可以用序列化的方式来实现对象的深克隆;序列化我们在学习javase基础的时候就已经学过了,用起来也是非常简单,只需要实现  Serializable 接口即可,通过 下面的代码例子可以看到,使用了更少的代码,并且不需要重写clone方法也可以实现对象深拷贝;

  1. package com.clone;
  2. import java.io.*;
  3. import java.util.ArrayList;
  4. import java.util.List;
  5. /**
  6. * 克隆对象--深拷贝--序列化拷贝
  7. */
  8. public class SerialCloneObject {
  9. public static void main(String[] args) throws Exception {
  10. List<String> subjectList = new ArrayList<>();
  11. subjectList.add("数学");
  12. subjectList.add("语文");
  13. Student zhangsan = new Student("张三", 15,subjectList);
  14. System.out.println("序列化前张三的地址:"+zhangsan);
  15. // 序列化
  16. ByteArrayOutputStream byteArrayOutputStream = serialObject(zhangsan);
  17. // 反序列化
  18. Object o = deserialObject(byteArrayOutputStream);
  19. // 打印结果
  20. System.out.println("序列化后张三的地址:"+o);
  21. Student student = (Student) o;
  22. //修改名称不会影响到深拷贝的对象
  23. System.out.println("将反序列化后的名称修改为:李四,并添加科目:体育");
  24. student.setName("李四");
  25. student.getSubjectList().add("体育");
  26. System.out.println("反序列化修改后的张三名称:"+student.getName());
  27. System.out.println("反序列化修改后的张三科目:"+student.getSubjectList());
  28. System.out.println("未序列化的张三科目:"+zhangsan.getSubjectList());
  29. }
  30. // 序列化
  31. private static ByteArrayOutputStream serialObject(Object object) throws IOException {
  32. ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
  33. ObjectOutputStream out = new ObjectOutputStream(byteArrayOutputStream);
  34. out.writeObject(object);
  35. // 关闭流
  36. byteArrayOutputStream.close();
  37. out.close();
  38. return byteArrayOutputStream;
  39. }
  40. // 反序列化
  41. private static Object deserialObject(ByteArrayOutputStream byteArrayOutputStream) throws IOException, ClassNotFoundException {
  42. ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
  43. ObjectInputStream in = new ObjectInputStream(byteArrayInputStream);
  44. Object o = in.readObject();
  45. // 关闭流
  46. byteArrayInputStream.close();
  47. in.close();
  48. return o;
  49. }
  50. }
  51. // 学生类
  52. class Student implements Serializable { // 实现Serializable 接口
  53. // 名称
  54. private String name;
  55. // 年龄,基本数据类型
  56. private int age;
  57. // 科目列表 ,引用类型
  58. private List<String> subjectList;
  59. public Student(String name,int age,List<String > subjectList){
  60. this.name = name;
  61. this.age = age;
  62. this.subjectList = subjectList;
  63. }
  64. public String getName() { return name; }
  65. public void setName(String name) { this.name = name; }
  66. public int getAge() { return age; }
  67. public void setAge(int age) { this.age = age; }
  68. public List<String> getSubjectList() { return subjectList; }
  69. public void setSubjectList(List<String> subjectList) { this.subjectList = subjectList; }
  70. }

打印结果

  1. 序列化前张三的地址:com.clone.Student@66d3c617
  2. 序列化后张三的地址:com.clone.Student@1ddc4ec2
  3. 将反序列化后的名称修改为:李四,并添加科目:体育
  4. 反序列化修改后的张三名称:李四
  5. 反序列化修改后的张三科目:[数学, 语文, 体育]
  6. 未序列化的张三科目:[数学, 语文]

 

关键字设计模式