深入浅出Java中的clone克隆方法


Java 中除了 调用构造函数来创建以外,还可以通clone来创建一个对象,而克隆又分为浅克隆和深度克隆,那么两者有什么区别呢?

浅克隆

我们先举一个例子,我们定义一个Person类,有 名字、年龄、地址对象

public class Address {
    // 公司地址
    private String companyAddress;
    // 家庭地址
    private String homeAddress;
    }
public class Person implements Cloneable{
    private String name;
    private int age;
    private Address address;
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", address=" + address +
                '}';
    }

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

那么怎样克隆一个对象呢,很简单 只要实现clone接口接口,然后调用clone()方法即可,下面我们看测试类:


public class PersonTest {
    public static void main(String[] args) throws CloneNotSupportedException {
        Person person = new Person();
        person.setName("zhangsan");
        person.setAge(20);

        Address address = new Address();
        address.setCompanyAddress("hefei");
        address.setHomeAddress("shanghai");
        person.setAddress(address);

        System.out.println("person" + person);
        //下面我们开始克隆一个对象
        Person person2 = person.clone();

        System.out.println("cloneperson" + person2);
    }

}

测试结果:person 对象被克隆出来了。

personPerson{name='zhangsan', age=20, address=Address{companyAddress='hefei', homeAddress='shanghai'}}
clonepersonPerson{name='zhangsan', age=20, address=Address{companyAddress='hefei', homeAddress='shanghai'}}
````java
我们试着修改clone的对象的地址,看原始的person会不会改变:
```java
        person2.getAddress().setCompanyAddress("beijing");
        System.out.println("person" + person);

原始的person 的 companyAddress 也被修改了地址。

其实原理非常简单,clone的对象根据原始对象在堆中开辟一块同等代销的内存,然后把原始对象的数据都复制到新的内存地址,对于基本类型,可以把原始的值直接复制过来,但是对于引用类型,其保存的只是一个地址,复制的也是对象的地址,最终还是指向同一个对象,所以修改任何一个都会影响另外一个。如下图:

在这里插入图片描述
我们从打印出来的两个Address对象的hashcode看,指向同一个对象
在这里插入图片描述
我们看下底层的的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);

实现浅拷贝的步骤是:

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

那么怎么对一个对象中的引用对象也进行拷贝呢?下面我们介绍深拷贝

深拷贝

我们还以上面的例子来讲解,我们在Address类中实现 clone接口:

public class Address implements Cloneable {
    // 公司地址
    private String companyAddress;
    // 家庭地址
    private String homeAddress;
    @Override
    protected Address clone() throws CloneNotSupportedException {
        return (Address) super.clone();
    }
}

然后再修改Person 类中的 clone 方法


    @Override
    protected Person clone() throws CloneNotSupportedException {
        Person p = (Person)super.clone();
        p.setAddress(p.getAddress().clone());
        return p ;
    }

测试结果如下:

在这里插入图片描述
但是这种方法存在一个问题,那就是一个对象中如果嵌套的对象比较多,我们还得把每个对象都实现Cloneable对象,那么有没有更简单的办法呢?

我们先将对象序列化,然后再反序列化新的对象。具体示例如下:

我们在Person 类中添加如下方法:

    public Object deepCopy(Object object) throws IOException, ClassNotFoundException {
        ByteArrayOutputStream bo = new ByteArrayOutputStream();
        ObjectOutputStream oo = new ObjectOutputStream(bo);
        oo.writeObject(object);

        ByteArrayInputStream bi = new ByteArrayInputStream(bo.toByteArray());
        ObjectInputStream oi = new ObjectInputStream(bi);
        return oi.readObject();
    }

但是 Address 类 和 Person都要实现 Serializable 接口,否则序列化失败;

测试代码如下:

public class Person implements Serializable{
    private String name;
    private int age;
    private Address address;
    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;
    }

    public Address getAddress() {
        return address;
    }

    public void setAddress(Address address) {
        this.address = address;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", address=" + address +
                '}';
    }


    public Object deepCopy(Object object) throws IOException, ClassNotFoundException {
        ByteArrayOutputStream bo = new ByteArrayOutputStream();
        ObjectOutputStream oo = new ObjectOutputStream(bo);
        oo.writeObject(object);

        ByteArrayInputStream bi = new ByteArrayInputStream(bo.toByteArray());
        ObjectInputStream oi = new ObjectInputStream(bi);
        return oi.readObject();
    }

}

测试结果:
personcom.md.Address@1540e19d
clonepersoncom.md.Address@6acbcfc0

深拷贝和浅拷贝的区别?

浅拷贝只会复制对象中基本数据类型数据和引用对象的内存地址,不会递归地复制引用对象,以及引用对象的引用对象……而深拷贝得到的是一份完完全全独立的对象。所以,深拷贝比起浅拷贝来说,更加耗时,更加耗内存空间。

如果要拷贝的对象是不可变对象,浅拷贝共享不可变对象是没问题的,但对于可变对象来说,浅拷贝得到的对象和原始对象会共享部分数据,就有可能出现数据被修改的风险,也就变得复杂多了。除非像我们今天实战中举的那个例子,需要从数据库中加载 10 万条数据并构建散列表索引,操作非常耗时,这种情况下比较推荐使用浅拷贝,否则,没有充分的理由,不要为了一点点的性能提升而使用浅拷贝。

克隆 和 new 的区别

  • java 中 clone 和 new 都可以创建 对象
  • clone不会调用构造方法;new 会调用构造方法
  • clone能快速创建一个已有对象的副本,即创建对象并且将已有对象中的属性值克隆;new只能在JVM中申请一个空的内存区域,对象的属性 值要通过构造方法赋值
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

半夏_2021

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值