从JDK角度看对象克隆

509 阅读6分钟

对象克隆

对象克隆其实是很常见的操作,它完成的功能是将现有对象内容(属性)拷贝到新的对象中,得到的是一个新的对象,而并不只是一个对象引用。

其实对于属性不多的对象我们可以直接通过编写代码逐一属性复制,比如我们可以直接 new 一个新对象,然后通过 set 方法将属性值一个个设置进去。但这种做法我们也是比较不屑,看起来不够高端,而且字段一多就会造成代码冗长。另外,可能有些私有变量也无法这样拷贝,所以克隆操作一般都使用 Java 内置的 Cloneable 接口实现。

简单例子

下面是一个简单的复制操作,对某个 Person 对象调用其 clone 方法即可以实现对象克隆。

public class Person implements Cloneable {

  public int age;

  public Person(int age) {
    this.age = age;
  }

  public Person clone() {
    Person o = null;
    try {
      o = (Person) super.clone();
    } catch (CloneNotSupportedException e) {
      e.printStackTrace();
    }
    return o;
  }
}

浅拷贝

因为是面向对象编程,所以对象在克隆过程中就会涉及到浅拷贝和深拷贝的问题。每个对象被创建后基本都会被一个引用变量来表示,这个引用指向了对象的地址,而当使用 Object 对象的 clone 方法进行克隆时,它会对原始数据类型的值直接复制一份新值,而如果对象的属性为引用类型时则会复制相应的引用值,所以此时复制的仅仅只是对象引用,克隆出来的对象的属性和原来对象的属性其实是指向同一个对象实例的。

直接通过下图更好理解,Person 对象包含 age、name 和 birDate 属性,name 为 String 类型的对象,而 birDate 为 Date 类型对象,那么通过默认的克隆策略克隆出来后为右边的 P_Copy 对象,name 和 birDate 属性都是指向原来 Person 对象属性指向的对象实例。

浅拷贝不是真的完全拷贝,它们可以各自修改自己的 age 属性而不会影响到彼此,但如果改动了 name 或 birDate 引用对象的值将会互相影响。它的优点是能节省内存空间。

这里写图片描述

public class Person implements Cloneable {

  public int age;
  private String name;
  private Date birDate;

  public Person(String name, int age) {
    this.age = age;
    this.name = name;
    this.birDate = new Date();
  }

  public Person clone() {
    Person o = null;
    try {
      o = (Person) super.clone();
    } catch (CloneNotSupportedException e) {
      e.printStackTrace();
    }
    return o;
  }
}

深拷贝

与浅拷贝对应的为深拷贝,既然默认的克隆策略是不能实现完成拷贝的,即不能将原来对象中的属性对象复制出一份新的副本。对于浅拷贝的节省内存空间,有时更需要的是克隆出完全互不影响的对象,这时就会用到深拷贝。

深拷贝的效果如下面的图所示,与浅拷贝相比,这时除了 age 属性外,name 和 birDate 属性也都有了自己的副本,达到了深拷贝的效果。

深拷贝属于真正的完全拷贝,它们可以各自修改自己的所有属性而不会影响到彼此。它的缺点是会消耗内存空间。

这里写图片描述

如下代码,要实现深拷贝就在 clone 方法中对需要拷贝的属性对象进行额外克隆并且赋值给对应的属性,这样就能实现深拷贝。

public class Person implements Cloneable {

  public int age;
  private String name;
  private Date birDate;

  public Person(String name, int age) {
    this.age = age;
    this.name = name;
    this.birDate = new Date();
  }

  public Person clone() {
    Person o = null;
    try {
      o = (Person) super.clone();
      o.birDate = (Date) this.birDate.clone();
    } catch (CloneNotSupportedException e) {
      e.printStackTrace();
    }
    return o;
  }

}

关于Cloneable接口

Cloneable接口没有定义任何方法,那么它有什么用呢?其实它的作用是为了标明哪些对象可以实现拷贝,实现了该接口的对象才能通过 JVM 执行克隆操作时的检查,没有实现该接口的会被抛出 CloneNotSupportedException 异常而无法进行克隆操作。

public interface Cloneable {
}

另外,还约定实现了 Cloneable 接口的类需要重写 Object 类的 clone 方法,重写该方法最简单的方式就是直接通过 super.clone() 调用 Object 的 clone方法。

Object的clone方法

Object的clone方法其实是一个本地方法,由本地方法表知道clone方法对应的本地函数为JVM_Clone,clone方法主要实现对象的克隆功能,根据该对象生成一个相同的新对象(我们常见的类的对象的属性如果是原始类型则会克隆值,但如果是对象则会克隆对象的地址)。

protected native Object clone() throws CloneNotSupportedException;

从代码中也解释了为什么需要实现Cloneable接口,if (!klass->is_cloneable())这里会校验是否有实现该接口,没有实现的则会抛 CloneNotSupportedException 异常。然后判断是否是数组分两种情况分配内存空间,新对象为new_obj,接着对new_obj进行copy及C++层数据结构的设置。最后再转成jobject类型方便转成Java层的Object类型。

JVM_ENTRY(jobject, JVM_Clone(JNIEnv* env, jobject handle))
  JVMWrapper("JVM_Clone");
  Handle obj(THREAD, JNIHandles::resolve_non_null(handle));
  const KlassHandle klass (THREAD, obj->klass());
  JvmtiVMObjectAllocEventCollector oam;

  if (!klass->is_cloneable()) {
    ResourceMark rm(THREAD);
    THROW_MSG_0(vmSymbols::java_lang_CloneNotSupportedException(), klass->external_name());
  }

  const int size = obj->size();
  oop new_obj = NULL;
  if (obj->is_javaArray()) {
    const int length = ((arrayOop)obj())->length();
    new_obj = CollectedHeap::array_allocate(klass, size, length, CHECK_NULL);
  } else {
    new_obj = CollectedHeap::obj_allocate(klass, size, CHECK_NULL);
  }
  Copy::conjoint_jlongs_atomic((jlong*)obj(), (jlong*)new_obj,
                               (size_t)align_object_size(size) / HeapWordsPerLong);
  new_obj->init_mark();

  BarrierSet* bs = Universe::heap()->barrier_set();
  assert(bs->has_write_region_opt(), "Barrier set does not have write_region");
  bs->write_region(MemRegion((HeapWord*)new_obj, size));

  if (klass->has_finalizer()) {
    assert(obj->is_instance(), "should be instanceOop");
    new_obj = instanceKlass::register_finalizer(instanceOop(new_obj), CHECK_NULL);
  }

  return JNIHandles::make_local(env, oop(new_obj));
JVM_END

序列化方式克隆

除了上述的通过 clone 方法来克隆外,还有一种方式可以实现克隆操作,即是序列化方式,将对象先序列化为二进制字节流,然后通过这些字节生成相同的属性值的对象。比如通过如下方式克隆一个对象,name 和 birDate 属性的引用与原来的对象属性的引用是不同的,对它们引用的对象进行修改是不会影响到原来的对象的属性的。

public class PersonSerialization implements Serializable {

  private static final long serialVersionUID = 4637638474632555808L;
  private String name;
  private int age;
  private Date birDate;

  public PersonSerialization(String name, int age) {
    this.name = name;
    this.age = age;
    this.birDate = new Date();
  }

  public PersonSerialization clone() {

    ByteArrayOutputStream byteOut = null;
    ObjectOutputStream objOut = null;
    ByteArrayInputStream byteIn = null;
    ObjectInputStream objIn = null;

    try {
      byteOut = new ByteArrayOutputStream();
      objOut = new ObjectOutputStream(byteOut);
      objOut.writeObject(this);
      byteIn = new ByteArrayInputStream(byteOut.toByteArray());
      objIn = new ObjectInputStream(byteIn);
      return (PersonSerialization) objIn.readObject();
    } catch (IOException | ClassNotFoundException e) {
      e.printStackTrace();
    } finally {
      try {
        byteIn = null;
        byteOut = null;
        if (objOut != null) objOut.close();
        if (objIn != null) objIn.close();
      } catch (IOException e) {}
    }
    return null;
  }
}

-------------推荐阅读------------

我的2017文章汇总——机器学习篇

我的2017文章汇总——Java及中间件

我的2017文章汇总——深度学习篇

我的2017文章汇总——JDK源码篇

我的2017文章汇总——自然语言处理篇

我的2017文章汇总——Java并发篇

------------------广告时间----------------

公众号的菜单已分为“分布式”、“机器学习”、“深度学习”、“NLP”、“Java深度”、“Java并发核心”、“JDK源码”、“Tomcat内核”等,可能有一款适合你的胃口。

鄙人的新书《Tomcat内核设计剖析》已经在京东销售了,有需要的朋友可以购买。感谢各位朋友。

为什么写《Tomcat内核设计剖析》

欢迎关注:

这里写图片描述