浅谈java对象浅度克隆和深度克隆

个人博客:www.letus179.com

需要克隆的原因

在工作中我们有时会遇到这样的需求:

A对象包含一些有用信息,这时候需要一个和A完全相同的B对象。拿到B对象后,只需要稍微调整下就ok。A和B是两个独立的对象,只是B的初始值来自于A。而A/B对象中包含了比较复杂的数据结构。此时通过简单的赋值,并不能满足这种需求。

我之前做过一个需求:

定时任务——自动新建XX产品:XX产品包含了很多信息,有基本类型数据也有复杂结构对象。已经上线的产品通过修改某些属性值就可以初始化形成新的XX产品,然后等待上线。这里就用到了克隆。


克隆的实现方式

浅度克隆

首先,定义一个Student类,包含两成员变量:nameage,并且实现Cloneable接口,代码如下:

public class Student implements Cloneable{

  private String name;

  private int age;

  public Student(String name, int age) {
    super();
    this.name = name;
    this.age = age;
  }

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public int getAge() {
    return age;
  }

  public void setAge(int age) {
    this.age = age;
  }
}

再,定义一个Teacher类,也是两字段:一个name,一个聚合对象student,并且实现Cloneable接口,代码如下:

public class Teacher implements Cloneable {

  private String name;

  private Student student;

  public Teacher(String name, Student student) {
    super();
    this.name = name;
    this.student = student;
  }

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public Student getStudent() {
    return student;
  }

  public void setStudent(Student student) {
    this.student = student;
  }
}

需要clone的类为什么要实现Cloneable接口?我们先看看该接口的源码:

package java.lang;

/**
 * A class implements the <code>Cloneable</code> interface to
 * indicate to the {@link java.lang.Object#clone()} method that it
 * is legal for that method to make a
 * field-for-field copy of instances of that class.
 * <p>
 * Invoking Object's clone method on an instance that does not implement the
 * <code>Cloneable</code> interface results in the exception
 * <code>CloneNotSupportedException</code> being thrown.
 * <p>
 * By convention, classes that implement this interface should override
 * <tt>Object.clone</tt> (which is protected) with a public method.
 * See {@link java.lang.Object#clone()} for details on overriding this
 * method.
 * <p>
 * Note that this interface does <i>not</i> contain the <tt>clone</tt> method.
 * Therefore, it is not possible to clone an object merely by virtue of the
 * fact that it implements this interface.  Even if the clone method is invoked
 * reflectively, there is no guarantee that it will succeed.
 *
 * @author  unascribed
 * @see     java.lang.CloneNotSupportedException
 * @see     java.lang.Object#clone()
 * @since   JDK1.0
 */
public interface Cloneable {
}

这是一个“标示接口”,即:没有任何方法和属性的接口。这个标示仅针对java.lang.Object#clone()方法。注释中:
第一段可以看到:我们调用的clone()方法是Obejct类的方法;
第二段中:若调用这个Object.clone()方法,但是不实现Cloneable接口(not implement Cloneable)的话,会抛CloneNotSupportedException异常。

我们来测试下,修改Student方法,去掉实现。代码修改如下:

public class Student {

    ...

  public static void main(String[] args) throws CloneNotSupportedException {
    Student student = new Student("jack", 27);
    Student cloneStudent = (Student) student.clone();
  }
}

执行结果如下:

Exception in thread "main" java.lang.CloneNotSupportedException: clone.Student
    at java.lang.Object.clone(Native Method)
    at clone.Student.main(Student.java:38)

第三段中说:按照惯例,对于Object.clone()方法,我们需要是重写。我们看下该方法源码:

protected native Object clone() throws CloneNotSupportedException;

是个native方法,一般来说native方法的效率要高于非native方法,因此比那种new出新对象再把旧对象的信息赋值到新对象的效率要高。该方法还是个protected方法,也就是说外部程序想调用有局限性,因此需要重写修饰符设置为public

下面再看一个Teacher相关的例子,这里先重写下equals()方法:

  @Override
  public boolean equals(Object obj) {
    if (obj == null) {
      return false;
    }
    if (this == obj) {
      return true;
    }

    if (obj instanceof Teacher) {
      Teacher teacher = (Teacher) obj;
      if (teacher.name == this.name && teacher.student.equals(this.student)) {
        return true;
      }
      return false;
    }
    return false;
  }

作如下测试:

public static void main(String[] args) throws CloneNotSupportedException {
    Student student = new Student("jack", 27);
    Teacher teacher = new Teacher("Ali", student);

    System.out.println("Teacher的内存地址:" + teacher);
    Teacher cloneTeacher = (Teacher) teacher.clone();
    System.out.println("克隆Teacher的内存地址:" + cloneTeacher);
    System.out.println("克隆前后,Teacher对象是否相等:" + teacher.equals(cloneTeacher) + "\n");

    System.out.println("++++++我只想修改克隆对象中的Student姓名为‘rose’++++++\n");
    cloneTeacher.getStudent().setName("rose");

    System.out.println("修改Student姓名后,Teacher的内存地址:" + teacher);
    System.out.println("修改Student姓名后,克隆Teacher的内存地址:" + cloneTeacher);
    System.out.println("修改Student姓名后,克隆前后,Teacher对象是否相等:" + teacher.equals(cloneTeacher));
  }

执行结果如下:

Teacher的内存地址:clone.Teacher@6c89db9a
克隆Teacher的内存地址:clone.Teacher@4eb09321
克隆前后,Teacher对象是否相等:true

++++++我只想修改克隆对象中的Student姓名为‘rose’++++++

修改Student姓名后,Teacher的内存地址:clone.Teacher@6c89db9a
修改Student姓名后,克隆Teacher的内存地址:clone.Teacher@4eb09321
修改Student姓名后,克隆前后,Teacher对象是否相等:true

我们可以看到:

  • 克隆前后,内存地址发生了变化@6c89db9a=>@4eb09321
  • 克隆前后以及修改了Student对象name属性值,对象没有变化,equals结果为true

也就是说,不同的引用指向了同一个对象。因此,若我只想修改克隆对象的信息,这种情况下是做不到的。

但是若只是修改基本类型或者String字符串,却能满足需求,只会影响克隆后的对象。譬如:
去掉:

 cloneTeacher.getStudent().setName("rose");

并增加:

cloneTeacher.setName("Alice");

equals结果:

修改Student姓名后,克隆前后,Teacher对象是否相等:false

最后别忘了重写下clone方法,修改修饰符为public:

  @Override
  public Object clone() throws CloneNotSupportedException {
    return super.clone();
  }

深度克隆

深度克隆之所以有深度,是弥补了浅度克隆对于对象类型的属性克隆的不足。这里通过对象序列化和反序列化来实现深度克隆。

对象的序列化

把对象转换为字节序列的过程

对象的反序列化:

把字节序列恢复为对象的过程

如何实现:
首先新建一个工具类,封装clone方法:

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class CloneUtil {

  private CloneUtil() {}

  @SuppressWarnings("unchecked")
  public static <T extends Serializable> T clone(T obj) {
    ByteArrayOutputStream baos = null;
    ObjectOutputStream oos = null;
    ByteArrayInputStream bais = null;
    ObjectInputStream ois = null;
    try {
      baos = new ByteArrayOutputStream();
      oos = new ObjectOutputStream(baos);
      oos.writeObject(obj);

      bais = new ByteArrayInputStream(baos.toByteArray());
      ois = new ObjectInputStream(bais);
      return (T) ois.readObject();
    }
    catch (ClassNotFoundException e) {
      throw new RuntimeException("Class not found.", e);
    }
    catch (IOException e) {
      throw new RuntimeException("Clone Object failed in IO.", e);
    }
    finally {
      try {
        if (ois != null) {
          ois.close();
        }
        if (oos != null) {
          oos.close();
        }
      }
      catch (IOException e) {
        e.printStackTrace();
      }
    }
  }
}

说明:

调用ByteArrayInputStream或ByteArrayOutputStream对象的close方法没有任何意义。这两个基于内存的流就能够释只要垃圾回收器清理对象放资源,这一点不同于对外部资源(如文件流)的释放。

修改Student代码如下:

public class Student implements Serializable{

  private static final long serialVersionUID = 1L;

  @Override
  public String toString() {
    return "Student [name=" + name + ", age=" + age + "]";
  }

  ...

}

修改Teacher代码如下:

public class Teacher implements Serializable {

  private static final long serialVersionUID = 1L;

  @Override
  public String toString() {
    return "Teacher [name=" + name + ", student=" + student + "]";
  }

  ...

}

替换main方法中测试代码:

 Teacher cloneTeacher = (Teacher) teacher.clone();

序列化-反序列化克隆方法:

    Teacher cloneTeacher = CloneUtil.clone(teacher);

执行结果:

Teacher的toString:Teacher [name=Ali, student=Student [name=jack, age=27]]
克隆Teacher的toString:Teacher [name=Ali, student=Student [name=jack, age=27]]
克隆前后,Teacher对象是否相等:false

++++++我只想修改克隆对象中的Student姓名为‘rose’++++++

修改Student姓名后,Teacher的的toString:Teacher [name=Ali, student=Student [name=jack, age=27]]
修改Student姓名后,克隆Teacher的的toString:Teacher [name=Ali, student=Student [name=rose, age=27]]
修改Student姓名后,克隆前后,Teacher对象是否相等:false

明显可以看到,修改Student属性后,对克隆前的对象没有影响。

需要注意:CloneUtil类中的clone()长这样:

public static <T extends Serializable> T clone(T obj) {
   ...
}

基于序列化和反序列化实现的克隆不仅仅是深度克隆,更重要的是通过泛型<T extends Serializable>限定,可以检查出要克隆的对象是否支持序列化,这项检查是编译器完成的,不是在运行时抛出异常,这种是方案明显优于使用Object类的clone方法克隆对象。让问题在编译的时候暴露出来总是好过把问题留到运行时。

当然,若是聚合对象如Teacher中的Student没有实现Serializable接口,还是会在运行时抛异常的。

  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值