Java进阶系列之对象克隆


大家都知道在Java中所有的类都是缺省的继承自Java语言包中的Object类的,查看它的源码,你可以把你的JDK目录下的src.zip复制到其他地方然后解压,里面就是所有的源码。发现里面有一个访问限定符为protected的方法clone():

protected native Object clone() throws CloneNotSupportedException;

仔细一看,它还是一个native方法,大家都知道native方法是非Java语言实现的代码,供Java程序调用的,因为Java程序是运行在JVM虚拟机上面的,要想访问到比较底层的与操作系统相关的就没办法了,只能由靠近操作系统的语言来实现。

为什么要克隆?

大家先思考一个问题,为什么需要克隆对象?直接new一个对象不行吗?答案是:克隆的对象可能包含一些已经修改过的属性,而new出来的对象的属性都还是初始化时候的值,所以当需要一个新的对象来保存当前对象的“状态”就靠clone方法了。那么我把这个对象的临时属性一个一个的赋值给我新new的对象不也行嘛?可以是可以,但是一来麻烦不说,二来,大家通过上面的源码都发现了clone是一个native方法,就是快啊,在底层实现的。提个醒,我们常见的Object a=new Object();Object b;b=a;这种形式的代码复制的是引用,即对象在内存中的地址,a和b对象仍然指向了同一个对象。而通过clone方法赋值的对象跟原来的对象时同时独立存在的。扯了这么多废话,终于进入正题了ORZ。

如何实现克隆

简单到你不敢相信。直接在你的类的后面声明implements Cloneable。关于这个接口,它的源码如下:

public interface Cloneable {
}

可以看到它是一个空的接口,它的作用就是做标记。如果没有实现Cloneable接口就直接使用clone方法,程序会抛出CloneNotSupportedException异常。
然后是重写clone方法,并修改成public访问级别。举个经典的栗子:

class Outer implements Cloneable {
    public int name;
    public Inner inner;
      
    @Override
    public Object clone() throws CloneNotSupportedException {
      return super.clone();
    }
public static void main(String[] args){
    Outer o_one = new Outer();
    o_one.inner = new Inner("zhangsan");
    try {
    Object obj = o_one.clone();
    Outer o_two = (Outer)obj;
    System.out.println(o_one==o_two);    //打印false
    System.out.println(o_one.inner.name.equals(o_two.inner.name)); //打印true
    } catch (CloneNotSupportedException e) {
     e.printStackTrace();
    }
}
}

上面的代码实现的其实是浅克隆,浅克隆对于引用类型仅拷贝引用,没有真正地让两个对象独立开来互相之间没有任何关系。由于是浅克隆,使得imp2修改了child的某个属性后会是的imp1中child的属性也跟着改变。或者比较两个对象的地址:imp1.child==imp2.child,返回的结果是true。但是,如果一个对象只包含原始数据域或者不可变对象域(比如String类型),推荐使用浅克隆。

深克隆

类中的所有引用类型做一些修改,让它也实现Cloneable接口。然后修改本类中的clone方法:

public class Inner implements  Cloneable{

  public String name;

  public Child(String name) {
      this.name = name;
  }

  @Override
  public String toString() {
      return "Inner的name值为:" + name;
  }

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

修改本类的clone方法:

static class Outer implements Cloneable {
  public int count;
  public Inner inner;
      
      
  @Override
  public Object clone() throws CloneNotSupportedException {
      Outer obj = (Outer)super.clone();
      obj.inner = (Inner) inner.clone();
      return obj;
  }
}

画重点了:

  • 需要重写clone方法,不仅仅只调用父类的方法,还需调用属性的clone方法;
  • 对象之间100%数据分离
  • 如果是对象存在引用类型的属性,建议使用深克隆
  • 深克隆比浅克隆要更加耗时,效率更低

解决多层克隆问题

如果引用类型里面还包含很多引用类型,或者内层引用类型的类里面又包含引用类型,使用clone方法就会很麻烦。这时我们可以用序列化的方式来实现对象的深克隆。

public class Outer implements Serializable{
  private static final long serialVersionUID = 369285298572941L;  //最好是显式声明ID
  public Inner inner;
  
  public Outer myclone() {
      Outer outer = null;
      try {
          ByteArrayOutputStream baos = new ByteArrayOutputStream();
          ObjectOutputStream oos = new ObjectOutputStream(baos);
          oos.writeObject(this);
  
          ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
          ObjectInputStream ois = new ObjectInputStream(bais);
          outer = (Outer) ois.readObject();
      } catch (IOException e) {
          e.printStackTrace();
      } catch (ClassNotFoundException e) {
          e.printStackTrace();
      }
      return outer;
  }
}

Inner也必须实现Serializable,否则无法序列化:

public class Inner implements Serializable{
  private static final long serialVersionUID = 872390113109L; //最好是显式声明ID
  public String name = "";

  public Inner(String name) {
      this.name = name;
  }

  @Override
  public String toString() {
      return "Inner的name值为:" + name;
  }
}

这样也能使两个对象在内存空间内完全独立存在,互不影响对方的值。

Tips

  • 在克隆方法中,如果我们需要对可变对象的final域也进行拷贝,由于final的限制,所以实际上是无法编译通过的。因此为了实现克隆,我们需要考虑舍去该可变对象域的final关键字。
  • 如果你决定用线程安全的类实现Cloneable接口,需要保证它的clone方法做好同步工作。默认的Object.clone方法是没有做同步的。

推荐阅读更多精彩内容