java克隆之深拷贝与浅拷贝

版权声明:本文出自汪磊的博客,未经作者允许禁止转载。

Java深拷贝与浅拷贝实际项目中用的不多,但是对于理解Java中值传递,引用传递十分重要,同时个人认为对于理解内存模型也有帮助,况且面试中也是经常问的,所以理解深拷贝与浅拷贝是十分重要的。

一、Java中创建对象的方式

①:与构造方法有关的创建对象方式

这是什么意思呢?比如我们new一个对象,其实就是调用对现象的有参或者无参的构造函数,反射中通过Class类的newInstance()方法,这种默认是调用类的无参构造方法创建对象以及Constructor类的newInstance方法,这几种方式都是直接或者间接利用对象的构造函数来创建对象的。

②:利用Object类中clone()方法来拷贝一个对象,方法定义如下:

protected native Object clone() throws CloneNotSupportedException;

看到了吧还是一个native方法,native方法是非Java语言实现的代码,通过JNI供Java程序调用。此处有个大体印象就可以了,具体此方法实现是由系统底层来实现的,我们可以在Java层调用此方法来实现拷贝的功能。

③:反序列化的方式

序列化:可以看做是将一个对象转化为二进制流的过程,通过这种方式把对象存储到磁盘文件中或者在网络上传输。

反序列化:可以看做是将对象的二进制流重新读取转换成对象的过程。也就是将在序列化过程中所生成的二进制串转换成对象的过程。

序列化的时候我们可以把一个对象写入到流中,此时原对象还在jvm中,流中的对象可以看作是原对象的一个克隆,之后我们在通过反序列化操作,就达到了对原对象的一次拷贝。

二、Java中基本类型与引用类型说明

此处必须理解,对理解深拷贝,浅拷贝至关重要。

基本类型也叫作值类型,说白了就是一个个简单的值,charbooleanbyte、short、int、long、float、double都属于基本类型,基本类型数据引用与数据均存储在栈区域,比如:

1 int a = 100;
2 int b = 234;

内存模型:

 

引用类型包括:类、接口、数组、枚举等。引用类型数据引用存储在栈区域,而值则存储在堆区域,比如:

1 String c = "abc";
2 String d = "dgfdere";

内存模型:

 

 

三、为什么要用克隆?

现在有一个Student类:

public class Student {

    private int age;

    public void setAge(int age) {
        this.age = age;
    }

    public int getAge() {
        return age;
    }
}

项目中有一个对象复制的需求,并且新对象的改变不能影响原对象。好了,我们撸起来袖子就开始写了,大意如下:

1 Student s1 = new Student();
2 s1.setAge(10);
3 Student s2 = s1;
4 System.out.println("s1:"+s1.getAge());
5 System.out.println("s2:"+s2.getAge());

打印信息如下:

s1:10
s2:10

一看打印信息信心更加爆棚了,完成任务。拿给项目经理看,估计经理直接让你去财务室结算工资了。。。。

上面确实算是复制了,但是后半要求呢?并且新对象的改变不能影响原对象,我们改变代码如下:

Student s1 = new Student();
s1.setAge(10);
Student s2 = s1;
System.out.println("s1:"+s1.getAge());
System.out.println("s2:"+s2.getAge());
//
s2.setAge(12);
System.out.println("s1:"+s1.getAge());
System.out.println("s2:"+s2.getAge());

打印信息如下:

s1:10
s2:10
s1:12
s2:12

咦?怎么s1对象的age也改变了呢?对于稍有经验的应该很容易理解,我们看一下内存模型:

看到了吧,Student s2 = s1这句代码在内存中其实是使s1,s2指向了同一块内存区域,所以后面s2的操作也影响了s1。

那怎么解决这个问题呢?这里就需要用到克隆了,克隆就是克隆一份当前对象并且保存其当前状态,比如当前s1的age是10,那么克隆对象的age同样也是10,相比较我们直接new一个对象这里就是不同点之一,我们直接new一个对象,那么对象中属性都是初始状态,还需要我们额外调用方法一个个设置比较麻烦,克隆的对象与原对象在堆内存中的地址是不同的,也就是两个不相干的对象,好了,接下来我们就该看看怎么克隆对象了。

四、浅拷贝

克隆实现起来比较简单,被复制的类需要实现Clonenable接口,不实现的话在调用对象的clone方法会抛出CloneNotSupportedException异常, 该接口为标记接口(不含任何方法), 覆盖clone()方法,方法中调用super.clone()方法得到需要的复制对象。

接下来我们改造Student类,如下:

 1 public class Student implements Cloneable {
 2 
 3     private int age;
 4 
 5     public void setAge(int age) {
 6         this.age = age;
 7     }
 8 
 9     public int getAge() {
10         return age;
11     }
12     
13     @Override
14     protected Object clone() throws CloneNotSupportedException {
15         // 
16         return super.clone();
17     }
18 }

继续改造代码:

 1 public class Main {
 2 
 3     public static void main(String[] args) {
 4         
 5         try {
 6             Student s1 = new Student();
 7             s1.setAge(10);
 8             Student s2 = (Student) s1.clone();
 9             System.out.println("s1:"+s1.getAge());
10             System.out.println("s2:"+s2.getAge());
11             //
12             s2.setAge(12);
13             System.out.println("s1:"+s1.getAge());
14             System.out.println("s2:"+s2.getAge());
15         } catch (CloneNotSupportedException e) {
16             // TODO Auto-generated catch block
17             e.printStackTrace();
18         }
19     }
20 }

主要就是第8行,调用clone方法来给s2赋值,相当于对s1对象进行了克隆,我们看下打印信息,如下:

s1:10
s2:10
s1:10
s2:12

看到了吧,s2改变其值而s1对象并没有改变,现在内存模型如下:

堆内存中是有两个对象的,s1,s2各自操作自己的对象,互不干涉。好了,到此上面的需求就解决了。

然而过了几天,业务有所改变,需要添加学生的身份信息,信息包含身份证号码以及住址,好吧,我们修改逻辑,新建身份信息类:

 1 public class IDCardInfo {
 2     //模拟身份证号码
 3     private String number;
 4     //模拟住址
 5     private String address;
 6 
 7     public String getNumber() {
 8         return number;
 9     }
10 
11     public void setNumber(String number) {
12         this.number = number;
13     }
14 
15     public String getAddress() {
16         return address;
17     }
18 
19     public void setAddress(String address) {
20         this.address = address;
21     }
22 
23 }

很简单,我们继续修改Student类,添加身份信息属性:

 1 public class Student implements Cloneable {
 2 
 3     private int age;
 4     //添加身份信息属性
 5     private IDCardInfo cardInfo;
 6 
 7     public void setAge(int age) {
 8         this.age = age;
 9     }
10 
11     public int getAge() {
12         return age;
13     }
14     
15     public IDCardInfo getCardInfo() {
16         return cardInfo;
17     }
18 
19     public void setCardInfo(IDCardInfo cardInfo) {
20         this.cardInfo = cardInfo;
21     }
22 
23     @Override
24     protected Object clone() throws CloneNotSupportedException {
25         // 
26         return super.clone();
27     }
28 }

以上没什么需要特别解释的,我们运行如下测试:

 1 public static void main(String[] args) {
 2         
 3         try {
 4             
 5             IDCardInfo card1 = new IDCardInfo();
 6             card1.setNumber("11111111");
 7             card1.setAddress("北京市东城区");
 8             Student s1 = new Student();
 9             s1.setAge(10);
10             s1.setCardInfo(card1);
11             Student s2 = (Student) s1.clone();
12             System.out.println("s1:"+s1.getAge()+","+s1.getCardInfo().getNumber()+","+s1.getCardInfo().getAddress());
13             System.out.println("s2:"+s2.getAge()+","+s2.getCardInfo().getNumber()+","+s2.getCardInfo().getAddress());
14             //
15             card1.setNumber("222222");
16             card1.setAddress("北京市海淀区");
17             s2.setAge(12);
18             System.out.println("s1:"+s1.getAge()+","+s1.getCardInfo().getNumber()+","+s1.getCardInfo().getAddress());
19             System.out.println("s2:"+s2.getAge()+","+s2.getCardInfo().getNumber()+","+s2.getCardInfo().getAddress());
20         } catch (CloneNotSupportedException e) {
21             // TODO Auto-generated catch block
22             e.printStackTrace();
23         }
24     }

主要逻辑就是给s1设置IDCardInfo信息,然后克隆s1对象赋值给s2,接下来改变card1信息,我们看下打印信息:

s1:10,11111111,北京市东城区
s2:10,11111111,北京市东城区
s1:10,222222,北京市海淀区
s2:12,222222,北京市海淀区

咦?怎么又出问题了,我们改变card1的信息,怎么影响了s2对象的身份信息呢?我们想的是只会影响s1啊,并且我们做了克隆技术处理。

到这里又引出两个概念:深拷贝与浅拷贝

以上我们处理的只是浅拷贝,浅拷贝会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是引用类型,拷贝的就是引用 ,由于拷贝的只是引用而不拷贝其对应的内存对象,所以拷贝后对象的引用类型的属性与原对象引用类型的属性还是指向同一对象,引用类型的属性对应的内存中对象不会拷贝,这里读起来比较绕,好好理解一下。

接下来我们看一下上面例子的内存模型:

看到了吧,就是s1,s2中IDCardInfo引用均指向了同一块内存地址,那怎么解决这个问题呢?解决这个问题就需要用到深拷贝了。

五、深拷贝

Object类中的clone是只能实现浅拷贝的,如果以上浅拷贝理解了,那么深拷贝也不难理解,所谓深拷贝就是将引用类型以及其指向的对象内存区域也一同拷贝一份,而不仅仅拷贝引用。

那怎么实现呢?以上面例子为例,要想实现深拷贝,那么IDCardInfo类也要实现Cloneable接口,并且重写clone()方法,修改如下:

 1 public class IDCardInfo implements Cloneable{
 2     //模拟身份证号码
 3     private String number;
 4     //模拟住址
 5     private String address;
 6 
 7     public String getNumber() {
 8         return number;
 9     }
10 
11     public void setNumber(String number) {
12         this.number = number;
13     }
14 
15     public String getAddress() {
16         return address;
17     }
18 
19     public void setAddress(String address) {
20         this.address = address;
21     }
22     
23     @Override
24     protected Object clone() throws CloneNotSupportedException {
25         //
26         return super.clone();
27     }
28 }

Student中clone()修改如下:

@Override
protected Object clone() throws CloneNotSupportedException {
    // 
    Student stu = (Student) super.clone();
    stu.cardInfo = (IDCardInfo) cardInfo.clone();
    return stu;
}

再次运行程序打印如下:

s1:10,11111111,北京市东城区
s2:10,11111111,北京市东城区
s1:10,222222,北京市海淀区
s2:12,11111111,北京市东城区

看到了吧,修改card1信息已经影响不到s2了,到此就实现了对象的深拷贝,此时内存模型如下:

 

大家想一下这样一个情节:A对象中有B对象的引用,B对象有C对象的引用,C又有D。。。。,尤其项目中引用三方框架中对象,要是实现深拷贝是不是特别麻烦,所有对象都要实现Cloneable接口,并且重写clone()方法,这样做显然是麻烦的,那怎么更好的处理呢?此时我们可以利用序列化来实现深拷贝。

六、序列化实现深拷贝

对象序列化是将对象写到流中,反序列化则是把对象从流中读取出来。写到流中的对象则是原始对象的一个拷贝,原始对象还存在 JVM 中,所以我们可以利用对象的序列化产生克隆对象,然后通过反序列化获取这个对象。

序列化的类都要实现Serializable接口,如果有某个属性不需要序列化,可以将其声明为transient。

接下来我们改造源程序通过序列化来实现深拷贝,IDCardInfo如下:

public class IDCardInfo implements Serializable{
    
    private static final long serialVersionUID = 7136686765975561495L;
    //模拟身份证号码
    private String number;
    //模拟住址
    private String address;

    public String getNumber() {
        return number;
    }

    public void setNumber(String number) {
        this.number = number;
    }

    public String getAddress() {
        return address;
    }

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

很简单就是让其实现Serializable接口。

Student改造如下:

public class Student implements Serializable {

    private static final long serialVersionUID = 7436523253790984380L;
    
    private int age;
    //添加身份信息属性
    private IDCardInfo cardInfo;

    public void setAge(int age) {
        this.age = age;
    }

    public int getAge() {
        return age;
    }
    
    public IDCardInfo getCardInfo() {
        return cardInfo;
    }

    public void setCardInfo(IDCardInfo cardInfo) {
        this.cardInfo = cardInfo;
    }

    //实现深拷贝
    public Object myClone() throws Exception{
        // 序列化
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(this);
        // 反序列化
        ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bis);

        return ois.readObject();
    }
}

同样让其实现Serializable接口,并且添加myClone()方法通过序列化反序列化实现其本身的深拷贝。

外部调用myClone()方法就可以实现深拷贝了,如下:

Student s2 = (Student) s1.myClone();

运行程序:

s1:10,11111111,北京市东城区
s2:10,11111111,北京市东城区
s1:10,222222,北京市海淀区
s2:12,11111111,北京市东城区

好了到此通过序列化同样实现了深拷贝。

七、克隆的实际应用

工作中很少用到深拷贝这块知识,我就说一个自己工作中用到的地方,最近写一个面向对象的网络请求框架,框架中有一个下载的功能,我们知道下载开始,进度更新,完毕,取消等都有相应的回调,在回调中我会传递出去一个下载信息的对象,这个对象包含下载文件的一些信息,比如:总长度,进度,已经下载的大小等等,这个下载信息向外传递就用到了克隆,我们只传递当前下载信息对象的一个克隆就可以了,千万别把当前下载信息直接传递出去,试想直接传递出去,外界要是修改了一些信息咋办,内部框架是会读取一些信息的,而我只克隆一份给外界,你只需要知道当前信息就可以了,不用你修改,你要是想修改那随便也影响不到我内部。

好了,以上就是关于克隆技术自己的总结,以及最后说了自己工作中用到的情形,本篇到此为止,希望对你有用。

声明:文章将会陆续搬迁到个人公众号,以后文章也会第一时间发布到个人公众号,及时获取文章内容请关注公众号

 

posted @ 2018-04-03 10:15  WangLei_ClearHeart  阅读(1354)  评论(3编辑  收藏  举报