0. 引子

如何复制一个类? 简单来说我们有一个Class:

public class CopyClass{
    int x;
    int y;

    public CopyClass(){
        x = 0;
        y = 0;
    }

    public int getX() {
        return x;
    }

    public void setX(int x) {
        this.x = x;
    }

    public int getY() {
        return y;
    }

    public void setY(int y) {
        this.y = y;
    }
}

简单来想,我们可以通过这样的操作来进行复制。

CopyClass copyClass1 = new CopyClass();
        CopyClass copyClass2 = copyClass1;

但是,当我们继续执行以下代码时:

copyClass2.setX(5);
        System.out.println("Class 1's X value is: " + copyClass1.getX());
        System.out.println("Class 2's X value is: " + copyClass2.getX());

会得到以下结果:

Class 1's X value is: 5
Class 2's X value is: 5

为什么?

copy集合 java java copy constructor_clone


实际上,我们只是把copyClass1所指向的在堆(Heap)中的地址赋给了copyClass2. 因此导致了改变了copyClass2.X的值后copyClass1.X的值也被改变。

1. 拷贝原型

为了在Heap中重新创建一片新的地址,我们有如下方案可以选择:
1. 直接用Setter和Getter复制

CopyClass copyClass1 = new CopyClass();
        CopyClass copyClass2 = new CopyClass();
        copyClass2.setX(copyClass1.getX());
        copyClass2.setY(copyClass1.getY());

  1. 或者在CopyClass中新建一个新的构造函数
public CopyClass(int x, int y){
        this.x = x;
        this.y = y;
    }

再执行以下代码:

CopyClass copyClass1 = new CopyClass();
        CopyClass copyClass2 = new CopyClass(copyClass1.getX(),copyClass1.getY());

能达到效果,但是太蠢。如果一个类有10个变量,那可读性就非常差了,而且传参时还容易出错。

2. 浅拷贝(Shallow Copy)

实际上,我们有如下方案可供选择:
- 拷贝构造函数(Copy Constructor)
- clone()方法

先说拷贝构造函数。

public CopyClass(CopyClass cc){
        this.x = cc.x;
        this.y = cc.y;
    }

调用

CopyClass copyClass1 = new CopyClass();
        CopyClass copyClass2 = new CopyClass(copyClass1);

执行测试代码:

copyClass2.setX(5);
        System.out.println("Class 1's X value is: " + copyClass1.getX());
        System.out.println("Class 2's X value is: " + copyClass2.getX());

得到结果:

Class 1's X value is: 0
Class 2's X value is: 5

可以看到此时拷贝正确。

再来说clone()方法。clone()是java.lang.Object中一个默认实现的protected方法。且,一般来说,以下三个表达式的返回值均为True.

x.clone() != x;//恒为真
x.clone().getClass() == x.getClass();//为真,如果x.clone()是调用Object的clone()来实现的话
x.clone().equals(x);//取决于equals()方法的实现

其源代码如下。

protected native Object clone() throws CloneNotSupportedException;

可以看到,这是一个JNI的方法。
打开OpenJDK的Object.c:

static JNINativeMethod methods[] = {
    {"clone",       "()Ljava/lang/Object;",   (void *)&JVM_Clone},
};

可以看到,clone()方法被映射为了JVM_Clone.返回类型是Object.
继续深究,找到jvm.cpp中对于JVM_Clone方法的实现。

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;

#ifdef ASSERT
  // Just checking that the cloneable flag is set correct
  if (obj->is_array()) {
    guarantee(klass->is_cloneable(), "all arrays are cloneable");
  } else {
    guarantee(obj->is_instance(), "should be instanceOop");
    bool cloneable = klass->is_subtype_of(SystemDictionary::Cloneable_klass());
    guarantee(cloneable == klass->is_cloneable(), "incorrect cloneable flag");
  }
#endif

  // Check if class of obj supports the Cloneable interface.
  // All arrays are considered to be cloneable (See JLS 20.1.5)
  if (!klass->is_cloneable()) {
    ResourceMark rm(THREAD);
    THROW_MSG_0(vmSymbols::java_lang_CloneNotSupportedException(), klass->external_name());
  }

  // Make shallow object copy
  const int size = obj->size();
  oop new_obj_oop = NULL;
  if (obj->is_array()) {
    const int length = ((arrayOop)obj())->length();
    new_obj_oop = CollectedHeap::array_allocate(klass, size, length, CHECK_NULL);
  } else {
    new_obj_oop = CollectedHeap::obj_allocate(klass, size, CHECK_NULL);
  }

  // 4839641 (4840070): We must do an oop-atomic copy, because if another thread
  // is modifying a reference field in the clonee, a non-oop-atomic copy might
  // be suspended in the middle of copying the pointer and end up with parts
  // of two different pointers in the field.  Subsequent dereferences will crash.
  // 4846409: an oop-copy of objects with long or double fields or arrays of same
  // won't copy the longs/doubles atomically in 32-bit vm's, so we copy jlongs instead
  // of oops.  We know objects are aligned on a minimum of an jlong boundary.
  // The same is true of StubRoutines::object_copy and the various oop_copy
  // variants, and of the code generated by the inline_native_clone intrinsic.
  assert(MinObjAlignmentInBytes >= BytesPerLong, "objects misaligned");
  Copy::conjoint_jlongs_atomic((jlong*)obj(), (jlong*)new_obj_oop,
                               (size_t)align_object_size(size) / HeapWordsPerLong);
  // Clear the header
  new_obj_oop->init_mark();

  // Store check (mark entire object and let gc sort it out)
  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_oop, size));

  Handle new_obj(THREAD, new_obj_oop);
  // Special handling for MemberNames.  Since they contain Method* metadata, they
  // must be registered so that RedefineClasses can fix metadata contained in them.
  if (java_lang_invoke_MemberName::is_instance(new_obj()) &&
      java_lang_invoke_MemberName::is_method(new_obj())) {
    Method* method = (Method*)java_lang_invoke_MemberName::vmtarget(new_obj());
    // MemberName may be unresolved, so doesn't need registration until resolved.
    if (method != NULL) {
      methodHandle m(THREAD, method);
      // This can safepoint and redefine method, so need both new_obj and method
      // in a handle, for two different reasons.  new_obj can move, method can be
      // deleted if nothing is using it on the stack.
      m->method_holder()->add_member_name(new_obj(), false);
    }
  }

  // Caution: this involves a java upcall, so the clone should be
  // "gc-robust" by this stage.
  if (klass->has_finalizer()) {
    assert(obj->is_instance(), "should be instanceOop");
    new_obj_oop = InstanceKlass::register_finalizer(instanceOop(new_obj()), CHECK_NULL);
    new_obj = Handle(THREAD, new_obj_oop);
  }

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

首先检查是不是Cloneable,所有的arrays被假定为是Cloneable.如果不是,抛出“CloneNotSupportedException” 。然后对该传入的对象进行浅拷贝,让GC整理后返回。
clone()方法的原理横跨了java和c++,看起来很复杂。但是源代码注释中提到了一件事情:浅拷贝(shallow copy)。我们暂且把这个问题留到3. 深拷贝的引子中叙述。
通过读源代码可以发现,要使用clone()方法必须implements Conleable. 与此同时,由于clone()是一个protected方法,我们不能直接调用。需要在具体的使用类中写一个方法,调用Object的clone()才能实现。
现在CopyClass这个类变成了这样:

public class CopyClass implements Cloneable{
    int x;
    int y;

    public CopyClass(){
        x = 0;
        y = 0;
    }

    public int getX() {
        return x;
    }

    public void setX(int x) {
        this.x = x;
    }

    public int getY() {
        return y;
    }

    public void setY(int y) {
        this.y = y;
    }

    public CopyClass(int x, int y){
        this.x = x;
        this.y = y;
    }

    public CopyClass(CopyClass cc){
        this.x = cc.x;
        this.y = cc.y;
    }

    @Override
    public Object clone()  {
        try {
            return super.clone();
        }
        catch (CloneNotSupportedException e){
            return new CopyClass(this);
        }
    }
}

下面执行测试工作。

CopyClass copyClass1 = new CopyClass();
        CopyClass copyClass2 = (CopyClass) copyClass1.clone();
        copyClass2.setX(5);
        System.out.println("Class 1's X value is: " + copyClass1.getX());
        System.out.println("Class 2's X value is: " + copyClass2.getX());

得到结果:

Class 1's X value is: 0
Class 2's X value is: 5

可以看到,拷贝构造函数和clone()方法均得到了正确的拷贝结果。

copy集合 java java copy constructor_System_02

3. 深拷贝(Deep Copy)

回到刚才的问题,什么是浅拷贝?深拷贝是与浅拷贝相对应的概念。
假设,我们再次修改CopyClass这个类。添加一个名为z的ZCoordinate的类变量。

public class CopyClass implements Cloneable{
    int x;
    int y;
    ZCoordinate z;

    public CopyClass(){
        x = 0;
        y = 0;
        z = new ZCoordinate();
    }

    public ZCoordinate getZ() {
        return z;
    }

    public void setZ(ZCoordinate z) {
        this.z = z;
    }
    //此处省去其余setter和getter
}

ZCoordinate类的构造

public class ZCoordinate {
    int z;

    public int getZ() {
        return z;
    }

    public void setZ(int z) {
        this.z = z;
    }

    public ZCoordinate(){
        z = 0;
    }
}

执行测试。

CopyClass copyClass1 = new CopyClass();
        CopyClass copyClass2 = (CopyClass) copyClass1.clone();
        copyClass2.getZ().setZ(5);
        System.out.println("Class 1's Z value is: " + copyClass1.getZ().getZ());
        System.out.println("Class 2's Z value is: " + copyClass2.getZ().getZ());

得到结果:

Class 1's Z value is: 5
Class 2's Z value is: 5

发现引子中的问题重现了。为什么? 因为如同之前clone()的源代码所说,clone()是一个浅拷贝。clone()方法只拷贝了z的引用地址,而不是z的所有变量。

copy集合 java java copy constructor_JVM_03


解决办法?Zcoordinate实现Copy Constructor,然后在CopyClass中的Copy Constructor调用Zcoordinate的Copy Constructor.

public ZCoordinate(ZCoordinate z){
        this.z = z.z;
    }
public CopyClass(CopyClass cc){
        this.x = cc.x;
        this.y = cc.y;
        this.z = new ZCoordinate(cc.z);
    }

测试:

CopyClass copyClass1 = new CopyClass();
        CopyClass copyClass2 = new CopyClass(copyClass1);
        copyClass2.getZ().setZ(5);
        System.out.println("Class 1's Z value is: " + copyClass1.getZ().getZ());
        System.out.println("Class 2's Z value is: " + copyClass2.getZ().getZ());

或者Zcoordinate实现clone()方法,然后修改CopyClass的clone()方法:

public Object clone()  {
        try {
            Object result = super.clone();
            ((CopyClass)result).z = (ZCoordinate) z.clone();
            return result;
        }
        catch (CloneNotSupportedException e){
            return new CopyClass(this);
        }
    }

测试:

CopyClass copyClass1 = new CopyClass();
        CopyClass copyClass2 = (CopyClass) copyClass1.clone();
        copyClass2.getZ().setZ(5);
        System.out.println("Class 1's Z value is: " + copyClass1.getZ().getZ());
        System.out.println("Class 2's Z value is: " + copyClass2.getZ().getZ());

结果均为:

Class 1's Z value is: 0
Class 2's Z value is: 5

可以看到,这样做很繁琐,如果一个数据类内部嵌套了许多层其他数据类结构,那么这种“深度拷贝”实现起来就是个灾难。
下一篇准备写写如何用其他方法快速实现深度拷贝。

4. 性能测试

测试对比Copy Constructor与clone()在一层深度拷贝下的效率。考虑以下测试用例:

public static void main(String[] args){
        final int SIZE = 1000000;
        final int ITERATE_TEIMES = 50;

        int totalOfCopyConstructor = 0;
        int totalOfClone = 0;
        long current;

        for (int i = 0; i < ITERATE_TEIMES; i++){
            current = System.currentTimeMillis();
            copyConstructor(SIZE);
            totalOfCopyConstructor += System.currentTimeMillis() - current;

            current = System.currentTimeMillis();
            clone(SIZE);
            totalOfClone += System.currentTimeMillis() - current;
        }

        System.out.println("Average Copy Constructor run time is : " + (totalOfCopyConstructor / ITERATE_TEIMES));
        System.out.println("Average clone run time is : " + (totalOfClone / ITERATE_TEIMES));
    }

    private static void copyConstructor(int size){
        for (int i = 0; i < size;i++){
            CopyClass copyClass1 = new CopyClass();
            CopyClass copyClass2 = new CopyClass(copyClass1);
        }
    }

    private static void clone(int size){
        for (int i = 0; i < size;i++){
            CopyClass copyClass1 = new CopyClass();
            CopyClass copyClass2 = (CopyClass) copyClass1.clone();
        }
    }

分别执行50次1百万次级的深度拷贝。取平均时间。可以得出下表:

次数

CopyConstructor

clone

1

6ms

16ms

2

6ms

18ms

3

7ms

21ms

嗯,结论是没事少用clone()…