深入理解Java浅拷贝与深拷贝

发表于:2024-3-27 09:35

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:springboot葵花宝典    来源:springboot葵花宝典

  浅拷贝和深拷贝是 Java 初中级面试中经常会被问到的一个问题,两个就像是兄弟俩,一个调皮一个乖巧,现在让我们一起来探索它们的奇妙之处!
  特别说明:不论是浅拷贝还是深拷贝,都可以使用Object类的clone方法来实现,代码如下:
  protected native Object clone() throws CloneNotSupportedException;
  注意:clone()方法也是一个本地方法,具体实现交给虚拟机,也就是说虚拟机在运行给方法时,就会变成搞笑的C/C++代码。
  1. 浅拷贝
  先让我们来了解一下浅拷贝。它就像是我们上学时抄了学霸一份作业,但结果可能让人出乎意料。为了演示这一点,我们创建了一个名为Student的类,这个学生有一个名为“name”的字符串字段和一个名为“age”的整数字段。
  public class Student implements  Cloneable{
      private int age;
      private String name;
      public Student(int age, String name) {
          this.age = age;
          this.name = name;
      }
      public int getAge() {
          return age;
      }
      public void setAge(int age) {
          this.age = age;
      }
      public String getName() {
          return name;
      }
      public void setName(String name) {
          this.name = name;
      }
      @Override
      public String toString() {
          return super.toString().substring(19) + "{" +
                  "age=" + age +
                  ", name='" + name + '\'' +
                  '}';
      }
      @Override
      public   Object clone() throws CloneNotSupportedException {
          return super.clone();
      }
  }
  接着,我们来进行测试。假设我们有一个名叫“springboot葵花宝典”的Student对象,并进行了浅拷贝。结果出来了!让我们看看会发生什么。
  public class CloneTest {
      public static void main(String[] args) throws CloneNotSupportedException {
          Student student1 = new Student(18,"springboot葵花宝典");
          Student student2 = (Student) student1.clone();
          System.out.println("浅拷贝后:");
          System.out.println("Student1:" + student1);
          System.out.println("Student2:" + student2);
          student2.setName("zbbmeta");
          System.out.println("调整了Student2 的 name  后:");
          System.out.println("Student1:" + student1);
          System.out.println("Student2:" + student2);
      }
  }
  测试结果如下:
  浅拷贝后:
  Student1:Student@4cb2c100{age=18, name='springboot葵花宝典'}
  Student2:Student@39fb3ab6{age=18, name='springboot葵花宝典'}
  调整了Student2 的 name  后:
  Student1:Student@4cb2c100{age=18, name='springboot葵花宝典'}
  Student2:Student@39fb3ab6{age=18, name='zbbmeta'}
  浅拷贝后,Student1 和 Student1 引用不同对象,但值是相同的,说明拷贝成功。之后,修改了 Student2 的 name 字段,student2的name和student1的name值不同。
  注意:一个类没有实现 Cloneable 接口,即便它重写了 clone() 方法,依然是无法调用该方法进行对象克隆的,会抛出异常CloneNotSupportedException。
  Exception in thread "main" java.lang.CloneNotSupportedException: com.zbbmeta.entity.Student
  思考:前面Student只有两个基本类型,没有引用类型,如果给Student添加一个自定义Book引用类型,浅拷贝会是什么结果?
  public class Book {
      private String bookName;
      private int price;
      public Book(String bookName, int price) {
          this.bookName = bookName;
          this.price = price;
      }
      //...   省略getter/setter 方法
    
      @Override
      public String toString() {
          return super.toString().substring(19) +
                  " bookName='" + bookName + '\'' +
                  ", price=" + price +
                  '}';
      }
  }
  重新编写Studnet类:
  public class Student implements  Cloneable{
      private int age;
      private String name;
      private Book book;
      public Student(int age, String name, Book book) {
          this.age = age;
          this.name = name;
          this.book = book;
      }
     //...   省略getter/setter 方法
      @Override
      public String toString() {
          return super.toString().substring(19) +
                  " age=" + age +
                  ", name='" + name + '\'' +
                  ", book=" + book +
                  '}';
      }
      @Override
      public   Object clone() throws CloneNotSupportedException {
          return super.clone();
      }
  }
  比之前的例子多了一个自定义类型的字段 book,clone() 方法并没有任何改变。
  测试类修改内容如下:
  public static void main(String[] args) throws CloneNotSupportedException {
          Student student1 = new Student(18,"springboot葵花宝典");
          Book book1 = new Book("springboot入门到精通",90);
          student1.setBook(book1);
          Student student2 = (Student) student1.clone();
          System.out.println("浅拷贝后:");
          System.out.println("Student1:" + student1);
          System.out.println("Student2:" + student2);
          Book book2 = student2.getBook();
          book2.setBookName("K8S实战");
          book2.setPrice(70);
          System.out.println("调整了Student2 的 book  后:");
          System.out.println("Student1:" + student1);
          System.out.println("Student2:" + student2);
      }
  输出结果如下:
  浅拷贝后:
  Student1:Student@6fb554cc age=18, name='springboot葵花宝典', book=Book@3c09711b bookName='springboot入门到精通', price=90}}
  Student2:Student@3a82f6ef age=18, name='springboot葵花宝典', book=Book@3c09711b bookName='springboot入门到精通', price=90}}
  调整了Student2 的 book  后:
  Student1:Student@6fb554cc age=18, name='springboot葵花宝典', book=Book@3c09711b bookName='K8S实战', price=70}}
  Student2:Student@3a82f6ef age=18, name='springboot葵花宝典', book=Book@3c09711b bookName='K8S实战', price=70}}
  student2.book 变更后,student1.book 也发生了改变。这是因为name字符串 String 是不可变对象,一个新的值必须在字符串常量池中开辟一段新的内存空间,而Book是自定义对象的内存地址并没有发生改变,只是对应的字段值发生了改变。
  总结:浅拷贝是创建一个新的对象,这个对象有着对原始对象属性值的一份精确拷贝。如果属性是基本数据类型,拷贝的就是基本数据类型的值;如果属性是引用数据类型,拷贝的就是内存地址,所以如果其中一个对象改变了引用类型的数据,就会影响另一个对象。
  2. 深拷贝
  深拷贝和浅拷贝不同的,深拷贝中的引用类型字段也会克隆一份,当改变任何一个对象,另外一个对象不会随之改变。例子如下:
  public class Book implements Cloneable{
      private String bookName;
      private int price;
      public Book(String bookName, int price) {
          this.bookName = bookName;
          this.price = price;
      }
   //...   省略getter/setter 方法
      @Override
      public String toString() {
          return super.toString().substring(19) +
                  " bookName='" + bookName + '\'' +
                  ", price=" + price +
                  '}';
      }
      @Override
      public Object clone() throws CloneNotSupportedException {
          return super.clone();
      }
  }
  注意:此时的 Book 类和浅拷贝时不同,重写了 clone() 方法,并实现了 Cloneable 接口。为的就是深拷贝的时候也能够克隆该字段。
  重新编写Studnet类:
  public class Student implements  Cloneable{
      private int age;
      private String name;
      private Book book;
      public Student(int age, String name, Book book) {
          this.age = age;
          this.name = name;
          this.book = book;
      }
     //...   省略getter/setter 方法
      @Override
      public String toString() {
          return super.toString().substring(19) +
                  " age=" + age +
                  ", name='" + name + '\'' +
                  ", book=" + book +
                  '}';
      }
      @Override
      public Object clone() throws CloneNotSupportedException {
          Student student = (Student) super.clone();
          student.setBook((Book) student.getBook().clone());
          return student;
      }
  }
  注意,此时 Student 类也与之前的不同,clone() 方法当中,不再只调用 Object 的 clone() 方法对 Student 进行克隆了,还对 Book 也进行了克隆。
  测试结果如下:
  浅拷贝后:
  Student1:Student@6fb554cc age=18, name='springboot葵花宝典', book=Book@3c09711b bookName='springboot入门到精通', price=90}}
  Student2:Student@3a82f6ef age=18, name='springboot葵花宝典', book=Book@100fc185 bookName='springboot入门到精通', price=90}}
  调整了Student2 的 book  后:
  Student1:Student@6fb554cc age=18, name='springboot葵花宝典', book=Book@3c09711b bookName='springboot入门到精通', price=90}}
  Student2:Student@3a82f6ef age=18, name='springboot葵花宝典', book=Book@100fc185 bookName='K8S实战', price=70}}
  发现: 不仅student1 和 student2 对象不同,它们中的 book 对象不同。所以,改变了 student2 中的 book 并不会影响到 student1。
  思考:嵌套的对象比较多的时,每一个类都需要重写clone()方法,这样拷贝起来就比较麻烦,那么有没有别的方法实现深拷贝。
  利用序列化,序列化是将对象写到流中便于传输,而反序列化则是将对象从流中读取出来。写入流中的对象就是对原始对象的拷贝。需要注意的是,每个要序列化的类都要实现 Serializable 接口,该接口和 Cloneable 接口类似,都是标记型接口。
  public class Book implements Cloneable{
      private String bookName;
      private int price;
      public Book(String bookName, int price) {
          this.bookName = bookName;
          this.price = price;
      }
   //...   省略getter/setter 方法
      @Override
      public String toString() {
          return super.toString().substring(19) +
                  " bookName='" + bookName + '\'' +
                  ", price=" + price +
                  '}';
      }
  }
  注意:Book 需要实现 Serializable 接口。
  重新编写Studnet类:
  public class Student implements  Cloneable{
      private int age;
      private String name;
      private Book book;
      public Student(int age, String name, Book book) {
          this.age = age;
          this.name = name;
          this.book = book;
      }
     //...   省略getter/setter 方法
      @Override
      public String toString() {
          return super.toString().substring(19) +
                  " age=" + age +
                  ", name='" + name + '\'' +
                  ", book=" + book +
                  '}';
      }
      //深度拷贝
      public Object deepClone() throws IOException, ClassNotFoundException {
          // 序列化
          ByteArrayOutputStream bos = new ByteArrayOutputStream();
          ObjectOutputStream oos = new ObjectOutputStream(bos);
          oos.writeObject(this);
          // 反序列化
          ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
          ObjectInputStream ois = new ObjectInputStream(bis);
          return ois.readObject();
      }
  }
  注意:Student 类实现 Serializable 接口,并且在该类中,增加了一个 deepClone() 的方法,利用 OutputStream 进行序列化,InputStream 进行反序列化,这样就实现了深拷贝。
  public static void main(String[] args) throws  IOException, ClassNotFoundException {
          Student student1 = new Student(18,"springboot葵花宝典");
          Book book1 = new Book("springboot入门到精通",90);
          student1.setBook(book1);
          Student student2 = (Student) student1.deepClone();
          System.out.println("浅拷贝后:");
          System.out.println("Student1:" + student1);
          System.out.println("Student2:" + student2);
          Book book2 = student2.getBook();
          book2.setBookName("K8S实战");
          book2.setPrice(70);
          System.out.println("调整了Student2 的 book  后:");
          System.out.println("Student1:" + student1);
          System.out.println("Student2:" + student2);
      }
  与之前测试类不同的是,调用了 deepClone() 方法。
  测试结果如下:
  浅拷贝后:
  Student1:Student@5dfcfece age=18, name='springboot葵花宝典', book=Book@5d5eef3d bookName='springboot入门到精通', price=90}}
  Student2:Student@5a8e6209 age=18, name='springboot葵花宝典', book=Book@4b4523f8 bookName='springboot入门到精通', price=90}}
  调整了Student2 的 book  后:
  Student1:Student@5dfcfece age=18, name='springboot葵花宝典', book=Book@5d5eef3d bookName='springboot入门到精通', price=90}}
  Student2:Student@5a8e6209 age=18, name='springboot葵花宝典', book=Book@4b4523f8 bookName='K8S实战', price=70}}
  测试结果和之前用 clone() 方法实现的深拷贝类似。
  特别说明:序列化涉及到输入流和输出流的读写,在性能上要比  虚拟机实现的 clone() 方法差很多。
  本文内容不用于商业目的,如涉及知识产权问题,请权利人联系51Testing小编(021-64471599-8017),我们将立即处理
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号