• Java深拷贝与序列化


    对基本类型的变量进行拷贝非常简单,直接赋值给另外一个对象即可:

    1 int b = 50;
    2 int a = b;  // 基本类型赋值

    对于引用类型的变量(例如 String),情况稍微复杂一些,因为直接等号赋值只是复制了一份引用,而复制前后的两个引用指向的是内存中的同一个对象。

    要想实现引用类型的拷贝,可以通过实现 Cloneable 接口,并覆盖其中的 clone 方法来实现。

    看一个例子,首先定义一个待拷贝的 Student 类,为简单起见,只设置了一个 name 属性

     1 class Student implements Cloneable{
     2     private String name;
     3 
     4     public String getName() {
     5         return name;
     6     }
     7 
     8     public void setName(String name) {
     9         this.name = name;
    10     }
    11     
    12     @Override
    13     public Object clone(){
    14         Student s = null;
    15         try{
    16             s = (Student)super.clone();
    17         }catch(Exception e){
    18             e.printStackTrace();
    19         }
    20         return s;
    21     }
    22 }
    Student

    可以看到,在 clone 方法里实际上是调用了 super.clone() 方法

    接下来对这个类进行复制,只需要调用 clone 方法即可:

    1 public void deepCopy(){
    2     Student s1 = new Student();
    3     s1.setName("zhang");
    4         
    5     Student s2 = (Student) s1.clone();
    6     s1.setName("wang");
    7     System.out.println(s1.getName());
    8     System.out.println(s2.getName());
    9 }
    deepCopy

    输出结果为:

    wang
    zhang

    由于s1修改了name属性值,输出的结果中s1和s2的name属性并不相同,说明这两个引用指向了不同的 Student 对象,实现了对象拷贝。

    但是,如果在Student中间添加一个引用对象,那么这种拷贝方式就会产生问题。

    为了说明问题,定义一个Car类,同样只有一个name属性:

     1 class Car{
     2     private String name;
     3 
     4     public String getName() {
     5         return name;
     6     }
     7 
     8     public void setName(String name) {
     9         this.name = name;
    10     }
    11 }
    Car类定义

    对 Student 类进行修改,添加一个 Car 类型的属性(略去这部分代码),在 deepCopy 方法里面对 Car 的 name 值进行修改,如下:

     1 public void deepCopy(){
     2     Student s1 = new Student();
     3     s1.setName("zhang");
     4     Car car = new Car();
     5     car.setName("Audi");
     6     s1.setCar(car);
     7         
     8     Student s2 = (Student) s1.clone();
     9     s1.setName("wang");
    10     car.setName("BMW");
    11     System.out.println(s1.getName());
    12     System.out.println(s2.getName());
    13     System.out.println(s1.getCar().getName());
    14     System.out.println(s2.getCar().getName());
    15 }
    修改后的deepCopy

    修改后的输出结果如下:

    wang
    zhang
    BMW
    BMW

    我们发现,对于 Car 类型的复制出现了问题,s1 和 s2 的Car属性的 name 值是相同的,都是修改后的 BMW,可以推测 s1 和 s2 的 Car 属性指向了内存中的同一个对象。通过s1.getCar() == s2.getCar() 进行验证,输出为 true,说明确实引用了同一个对象。

    出现问题的原因是,上面的方法是浅拷贝方法。所谓浅拷贝,是指拷贝对象的时候只是对其中的基本类型属性进行复制,而并不拷贝对象中的引用属性。而我们想要实现的效果是连同 Student 中的引用类型属性一起复制,这就是深拷贝。深拷贝是一个整个独立的对象拷贝,深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大

    为了解决这个问题,一种可行的方式是让 Car 类也实现 Cloneable 接口,并覆盖 clone 方法,在 Student 类的 clone 方法里加上一行代码:

    this.car = (Car)car.clone()

    这样的确能够解决 Car 没有复制的问题,然而如果 Student 中有多个引用类型属性,这些对象有可能也会有其他的引用类型属性,那么上面这种做法就要去所有的相关类都要实现 Cloneable 接口,并覆盖 clone 方法,不仅麻烦,而且非常不利于后期维护和扩展。

    一种比较优雅的做法是利用 Java 的序列化和反序列化实现深拷贝。序列化是指将对象转换成字节序列的过程,反序列化是指将字节序列还原成对象的过程。一般在对象持久化保持或者进行网络传输的时候会用到序列化。【需要注意的是 static 和 transient 类型的变量不会被序列化】

    利用序列化和反序列化进行深拷贝比较简单,只需要实现 Serializable 接口就行。我们对Student类就行修改,如下:

     1 class Student implements Serializable{
     2     
     3     //private static final long serialVersionUID = 1L;
     4     
     5     private String name;
     6     private Car car;
     7 
     8     public Car getCar() {
     9         return car;
    10     }
    11 
    12     public void setCar(Car car) {
    13         this.car = car;
    14     }
    15 
    16     public String getName() {
    17         return name;
    18     }
    19 
    20     public void setName(String name) {
    21         this.name = name;
    22     }
    23 }
    修改后的Student

    这里暂时忽略其中的 serialVersionUID 属性,让Car类也同样实现 Serializable 接口,之后定义一个深拷贝的方法:

     1 public void deepCopyWithSerialize(){
     2     Student s1 = new Student();
     3     s1.setName("zhang111");
     4     Car car = new Car();
     5     car.setName("Audi");
     6     s1.setCar(car);
     7         
     8     ObjectOutputStream oo;
     9     try {
    10         oo = new ObjectOutputStream (new FileOutputStream("a.txt"));
    11         oo.writeObject(s1);
    12         oo.close();
    13             
    14         ObjectInputStream ois = new ObjectInputStream(new FileInputStream("a.txt")); 
    15         Student s2 = (Teacher) ois.readObject();
    16             
    17         s1.setName("wahah");
    18         car.setName("BMW");
    19         System.out.println(s1.getName());
    20         System.out.println(s2.getName());
    21         System.out.println(s1.getCar().getName());
    22         System.out.println(s2.getCar().getName());
    23     } catch (IOException e) {
    24         // TODO Auto-generated catch block
    25         e.printStackTrace();
    26     } catch (ClassNotFoundException e) {
    27         // TODO Auto-generated catch block
    28         e.printStackTrace();
    29     }
    30         
    31 }
    deepCopyWithSerialize

    输出结果为:

    wahah
    zhang111
    BMW
    Audi
    输出结果

    可以看出,成功实现了对象的深拷贝。这里选择了利用文件来保存序列化的对象,也可以选择其他的形式,例如 ByteArrayOutputStream 

    1 ByteArrayOutputStream baos = new ByteArrayOutputStream();
    2 ObjectOutputStream oos = new ObjectOutputStream(baos);
    3 oos.writeObject(s1);           
    4             
    5 // 从流中读出对象
    6 ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
    7 ObjectInputStream  ois = new ObjectInputStream(bais)
    8 Student s2 = ois.readObject();
    ByteArrayOutputStream序列化

    接下来解释一下刚才忽略的 serialVersionUID,根据名字知道这是一个与对象的状态有关的变量,如果代码中没有定义这样的变量,那么在运行的时候会按照一定的方式自动生成,在反序列化的时候会对这个值进行判断,如果两个值不相等,会抛出 InvalidClassException 。由于计算默认的 serialVersionUID 对类的详细信息具有较高的敏感性,一般建议在序列化的时候主动提供这个参数。

    【总结】

    ① Cloneable 接口的 clone 方法默认是浅拷贝,需要自行覆盖才能实现深拷贝。

    ② 使用 Serializable 序列化的方式实现深拷贝比较简单,但是需要注意定义 serialVersionUID 的值,并且 static 和 transient 类型的变量不会被序列化。

    【参考资料】

    本文的内容主要参考了以下的博客,在此表示感谢

    Benson的专栏-Java如何复制对象

    田木木-深克隆

    请叫我大师兄-Java 之 Serializable 序列化和反序列化的概念,作用的通俗易懂的解释

    Java中的关键字 transient

    Java序列化之排除被序列化字段(transient/静态变量)

  • 相关阅读:
    编写Music类
    Double-checked locking and the Singleton pattern--双重检查加锁失效原因剖析
    C语言中,数组名作为参数传递给函数时,退化为指针
    蘑菇街笔试
    动态规划--股市买入卖出时间点选择问题
    Java 查看死锁的方法
    linux中shell变量$#,$@,$0,$1,$2的含义解释
    Spring中bean的配置
    为什么要使用连接池
    Hadoop Bloom filter应用示例
  • 原文地址:https://www.cnblogs.com/NaLanZiYi-LinEr/p/9192734.html
Copyright © 2020-2023  润新知