Java 杂谈Java技术分享

什么是对象克隆(拷贝/复制)?浅克隆与深克隆有什么区别?

2018-10-09  本文已影响1人  Belmode

一、什么是对象克隆?

首先我们需要知道,什么是对象的克隆,或者说复制。一个业务逻辑,需要一个新的对象,但是类型和值都是之前的,也就是说,新状态和之前完全一样。使用new和赋值语句或者set注入都是可以的,但是,这会花费大量开销去做,效率低,并且还会产生冗余代码。
恰好java语言本身契合了原型设计模式,给我们提供了一个clone方法在Object对象中,只要需要克隆的对象实现Cloneable接口,那么我们只要简单的调用一下该方法就可以获得表面完全不同的对象。
例1:

public class User implements Cloneable{
    @Override
    protected Object clone() throws CloneNotSupportedException {
        // TODO Auto-generated method stub
        return super.clone();
    }
}
......
User u = new User();
User uCopy = u.clone();
//显然u和uCopy已经是两个不同的对象了
System.out.println(u == uCopy);//false

二、如何实现对象克隆

上文也是提到,克隆对象基本只要两步:

  1. 实现Cloneable接口
  2. 实现clone()方法,并调用父类clone()

需要注意,Objectclone()方法是在java平台层实现的native方法,具有开销小,速度快的特点。而且,原始的Object方法是被protected修饰的,在这里需要修改为public,如果不这么做,浅克隆时没有问题,深克隆就会遇到权限不够的问题。java继承还有个原则,就是子类覆写父类方法,访问修饰符权限不能低于父类。

三、什么是浅克隆和深克隆

例2:

/**
 * 浅克隆/浅拷贝
 * @datetime  2018-10-09 09:43:22
 * @author Belmode
 *
 */
public class ShallowClone {
    public static void main(String[] args) throws Exception {
        Person p = new Person("李达康", "55", "汉东省京州市");
        Person p2 = (Person) p.clone();
        //一级对象
        System.out.println(p == p2);//false
        //二级对象
        System.out.println(p.personalInfo == p2.personalInfo);//true
        //三级对象
        System.out.println(p.personalInfo.name == p2.personalInfo.name);//true
    }
}
class PersonalInfo implements Cloneable{
    String name;
    String age;
    String address;
    public PersonalInfo(String name, String age, String address) {
        this.name = name;
        this.age = age;
        this.address = address;
    }
    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
class Person implements Cloneable{
    PersonalInfo personalInfo;
    
    public Person(String name, String age, String address) {
        this.personalInfo = new PersonalInfo(name, age, address);
    }
    
    @Override
    public Object clone() throws CloneNotSupportedException {
         return super.clone();
    }
}

在本例中,Person类拥有PersonalInfo类作为属性,两者都实现了Cloneable接口,并且重写了clone()方法。
ShallowClone类的main方法中,构建了一个Person对象p,调用clone()方法,产生p2pp2不是一个对象,但是pp2内部的personalInfo属性是同一对象,这就会导致一个致命的问题,当我修改p.personalInfo内部的属性值,结果p2.personalInfo也被修改了。
p.personalInfo.name = "孙正义";
System.out.println(p2.personalInfo.name); //孙正义

在JVM虚拟机中,事实上就是一个java.exe或者javaw.exe进程里, 会存在栈内存和堆内存,堆内存又可细分,这里就不多说了,想深入了解的可以问度娘。在方法区中存储着PersonPersonalInfo的类模板,通过new 派生出 ppesonalInfo的实例,如图中箭头所示。此时p里引用着personalInfo实例(其实应该从p实例里的方框画箭头到栈位置的personalInfo标签,这里为了下面说明,就没那么做了)
在栈内存中存储的都是hashcode,类似于eb5238b,存在p的位置,就产生了图中的箭头

图2:调用clone()方法,克隆出p2


如图所示,p2是真的新实例,但是personalInfo也是复制的值,p2里的personalInfo属性保存personalInfo实例的地址值,所以,如黄线所示,还是引用到原本的personalInfo实例。
因此,修改p.personalInfo里的属性,p2.personalInfo里的值也会收到影响!!

那么,如果我修改p.personalInfo属性,p.personalInfo = new PersonalInfo.....p2.pernalInfo会不会收到影响呢?
答案是不会的。
图3:

如图所示,会断开红叉处的连接,生成橙色连接。如橙线所示。
p对象和p2personalInfo属性其实已经不同了。

话题有点跑偏了,因为会出现上面的原因,所以需要深克隆。

例3:

public class DeepClone {
    public static void main(String[] args) throws Exception {
        Person2 person = new Person2("李达康", "55", "汉东省京州市");
        Person2 person2 = (Person2) person.clone();
        //一级对象
        System.out.println(person == person2);//false
        //二级对象
        System.out.println(person.personalInfo == person2.personalInfo);//false
        //三级对象
        System.out.println(person.personalInfo.name == person2.personalInfo.name);//false
    }
}

class PersonalInfo2 implements Cloneable{
    String name;
    String age;
    String address;
    public PersonalInfo2(String name, String age, String address) {
        this.name = name;
        this.age = age;
        this.address = address;
    }
    
    @Override
    public Object clone() throws CloneNotSupportedException {
        PersonalInfo2 pInfo = (PersonalInfo2) super.clone();
        pInfo.name = new String(this.name);
        pInfo.age = new String(this.age);
        pInfo.address = new String(this.address);
        return pInfo;
    }
}
class Person2 implements Cloneable{
    PersonalInfo2 personalInfo;
    
    public Person2(String name, String age, String address) {
        this.personalInfo = new PersonalInfo2(name, age, address);
    }
    
    @Override
    public Object clone() throws CloneNotSupportedException {
       Person2 p = (Person2) super.clone();
       p.personalInfo = (PersonalInfo2) this.personalInfo.clone();
       return p;
    }
}

在这里,Person2调用父类clone(),克隆出新实例p,也给p.personalInfo调用 PersonalInfo2的父类方法,而在此中

    @Override
    public Object clone() throws CloneNotSupportedException {
        PersonalInfo2 pInfo = (PersonalInfo2) super.clone();
        pInfo.name = new String(this.name);
        pInfo.age = new String(this.age);
        pInfo.address = new String(this.address);
        return pInfo;
    }

对所有引用类型String全部new产生和之前不同的对象,达到深度克隆的目的。
(因为String没有实现Cloneable接口,需要通过new派生,这里是举特殊例子。如果是实现Cloneable接口的类,直接嵌套调用clone()方法就好了。)
这样,新的Person2实例,与之前的引用完全不同,但是值是一样的!深度克隆成功。

        Person2 person = new Person2("李达康", "55", "汉东省京州市");
        Person2 person2 = (Person2) person.clone();
        //一级对象
        System.out.println(person == person2);//false
        //二级对象
        System.out.println(person.personalInfo == person2.personalInfo);//false
        //三级对象
        System.out.println(person.personalInfo.name == person2.personalInfo.name);//false

        System.out.println(person.personalInfo.name);//李达康
        System.out.println(person2.personalInfo.name);//李达康

三、还有其他深度克隆方式吗?

对象序列化与反序列化。但是这么做开销更大,不建议如此。(这里不做举例)

总结:

1.浅克隆:只复制基本类型的数据,引用类型的数据只复制了引用的地址,引用的对象并没有复制,在新的对象中修改引用类型的数据会影响原对象中的引用。直接使用clone方法,再嵌套的还是浅克隆,因为有些引用类型不能直接克隆。
2.深克隆:是在引用类型的类中也实现了clone,是clone的嵌套,并且在clone方法中又对没有clone方法的引用类型又做差异化复制,克隆后的对象与原对象之间完全不会影响,但是内容完全相同。
3.使用序列化也能完成深复制的功能:对象序列化后写入流中,此时也就不存在引用什么的概念了,再从流中读取,生成新的对象,新对象和原对象之间也是完全互不影响的。

上一篇下一篇

猜你喜欢

热点阅读