蹲厕所的熊

benjaminwhx

Java中的深浅拷贝

2018-06-06 作者: 吴海旭


  1. 浅拷贝
  2. 深拷贝

Java中创建对象除了调用构造函数来创建以外,还有几种利用语言之外的机制来创建的方式,clone就是其中的一种(还有一种就是前面说过的序列化的方式)。

比如我们有一个Person类,有两个属性,一个年龄age,一个姓名name(又是熟悉的Person~)

@Data
@RequiredArgsConstructor
public class Person {
    @NonNull private int age;
    @NonNull private String name;
}

new出一个对象

Person p = new Person(20, "Alex");

我们希望找一个和Alex一样的人

Person p2 = p;

但是这样的话,p2其实只是复制了p的引用,p2和p还是同一个人。

clone1

我们要想真正复制一个对象可以重新new一个对象,然后把它的属性都copy一次,像这样:

Person p2 = new Person();
p2.setAge(p.getAge());
p2.setName(p.getName());

clone2

好了,全新的Alex出来了。但是如果属性很多的话,写的会很麻烦。于是浅拷贝来了。

浅拷贝

浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。

我们用浅拷贝改写一下Person类。

@Data
@RequiredArgsConstructor
public class Person implements Cloneable {
    @NonNull private int age;
    @NonNull private String name;

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

这样它就就能轻松的复制出对象了。

Person p2 = p.clone();

我们试着增加一个对象Pet(Alex养的宠物),对象变成了这样:

@Data
public class Pet {
    private String petName;
}

@Data
@RequiredArgsConstructor
public class Person implements Cloneable {
    @NonNull private int age;
    @NonNull private String name;
    private Pet pet;

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

这个时候我们还是像刚刚那样clone出来一个Person对象,但是这次我们会改变Pet的名字,看看会有什么发生。

public class PersonTest {

    public static void main(String[] args) throws CloneNotSupportedException {
        Person p = new Person(20, "Alex");
        Pet pet = new Pet();
        pet.setPetName("wangcai");
        p.setPet(pet);

        Person p2 = p.clone();
        p2.getPet().setPetName("huahua");
        // huahua
        System.out.println(p.getPet().getPetName());
    }
}

结果令人惊讶,p的宠物名字被clone出来的p2改变了。

其实原理很简单,clone的对象根据原始对象从堆中开辟一块同等大小的内存,然后把原始对象的数据都复制到新的内存地址,对于基本类型,可以把原始值复制过来,但是对于引用类型,其保存的只是一个地址,复制时也是对地址的复制,最终还是指向同一个对象,所以也就造成了上面的问题。

这里贴出部分clone的源码(jvm.cpp中搜“JVM_clone”)

// 判断复制的类是否实现Cloneable接口
if (!klass->is_cloneable() ||
      (klass->is_instance_klass() &&
       InstanceKlass::cast(klass)->reference_type() != REF_NONE)) {
    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);
  }

  HeapAccess<>::clone(obj(), new_obj_oop, size);

  Handle new_obj(THREAD, new_obj_oop);

实现浅拷贝的步骤是:

  1. 被复制的类需要实现Cloneable接口(不实现的话在调用clone方法会抛出CloneNotSupportedException异常)改接口为标记接口(不含任何方法)
  2. 覆盖clone()方法,访问修饰符设为public。方法中调用super.clone()方法得到需要的复制对象,(native为本地方法)

深拷贝

深拷贝就是把一个对象中的所有对象都拷贝一遍。

试着改写一下上面的例子。

@Data
public class Pet implements Cloneable {
    private String petName;

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

@Data
@RequiredArgsConstructor
public class Person implements Cloneable {
    @NonNull private int age;
    @NonNull private String name;
    private Pet pet;

    @Override
    protected Person clone() throws CloneNotSupportedException {
        Person cloneP = (Person) super.clone();
        cloneP.setPet(pet.clone());
        return cloneP;
    }
}

测试结果印证了我们的猜想,p2的宠物改名并没有影响p的宠物名。

但是这样写还会存在一个问题,如果一个对象中嵌套的对象比较多,我们还得去把每一个对象都实现Cloneable接口,如果对象里面嵌套对象,clone方法的处理也会比较复杂,有没有一种简单的方式去实现呢?

还记得我们之前学习过的序列化方式吗?通过输入输出流来实现对象的深拷贝,这也是我们最开始提到的创建对象的另一种方式。

如果还没来得及看序列化相关的知识的话也没关系,apache的commons-lang3包已经帮我们实现了这种方式:SerializationUtils.clone(T object),我们再来改改代码:

@Data
public class Pet implements Serializable {
    private static final long serialVersionUID = 503983247316042185L;
    private String petName;
}

@Data
@RequiredArgsConstructor
public class Person implements Serializable {
    private static final long serialVersionUID = 8285119178112680095L;
    @NonNull private int age;
    @NonNull private String name;
    private Pet pet;
}

最后写一个测试类印证我们的猜想:

public class PersonTest {

    public static void main(String[] args) throws CloneNotSupportedException {
        Person p = new Person(20, "Alex");
        Pet pet = new Pet();
        pet.setPetName("wangcai");
        p.setPet(pet);

        Person p2 = SerializationUtils.clone(p);
        p2.getPet().setPetName("huahua");
        // wangcai
        System.out.println(p.getPet().getPetName());
    }
}

Perfect~



坚持原创技术分享,您的支持将鼓励我继续创作!



分享

评论