(轉)java 深克隆(深拷貝)與淺克隆(淺拷貝)詳解

java深克隆和淺克隆

基本概念

淺複製(淺克隆)
被複制對象的所有變量都含有與原來的對象相同的值,而所有的對其他對象的引用仍然指向原來的對象。換言之,淺複製僅僅複製所拷貝的對象,而不復制它所引用的對象。
深複製(深克隆)

被複制對象的所有變量都含有與原來的對象相同的值,除去那些引用其他對象的變量。那些引用其他對象的變量將指向被複制過的新對象,而不再是原有的那些被引用的對象。換言之,深複製把要複製的對象所引用的對象都複製了一遍。
實現java深複製和淺複製的最關鍵的就是要實現Cloneable接口中的clone()方法。

如何使用clone()方法

首先我們來看一下Cloneable接口:

官方解釋:
1:實現此接口則可以使用java.lang.Object 的clone()方法,否則會拋出CloneNotSupportedException 異常
2:實現此接口的類應該使用公共方法覆蓋clone方法
3:此接口並不包含clone 方法,所以實現此接口並不能克隆對象,這只是一個前提,還需覆蓋上面所講的clone方法。
public interface Cloneable {
}
1
2
看看Object裏面的Clone()方法:

clone()方法返回的是Object類型,所以必須強制轉換得到克隆後的類型
clone()方法是一個native方法,而native的效率遠遠高於非native方法,
可以發現clone方法被一個Protected修飾,所以可以知道必須繼承Object類才能使用,而Object類是所有類的基類,也就是說所有的類都可以使用clone方法
protected native Object clone() throws CloneNotSupportedException;
1
小試牛刀:

public class Person {
    public void testClone(){
        super.clone(); // 報錯了
    }
}
1
2
3
4
5
事實卻是clone()方法報錯了,那麼肯定奇怪了,既然Object是一切類的基類,並且clone的方法是Protected的,那應該是可以通過super.clone()方法去調用的,然而事實卻是會拋出CloneNotSupportedException異常, 官方解釋如下:

對象的類不支持Cloneable接口
覆蓋方法的子類也可以拋出此異常表示無法克隆實例。
所以我們更改代碼如下:

public class Person implements Cloneable{
    public void testClone(){
        try {
            super.clone();
            System.out.println("克隆成功");
        } catch (CloneNotSupportedException e) {
            System.out.println("克隆失敗");
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        Person p = new Person();
        p.testClone();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
要注意,必須將克隆方法寫在try-catch塊中,因爲clone方法會把異常拋出,當然程序也要求我們try-catch。

java.lang.object規範中對clone方法的約定

對任何的對象x,都有x.clone() !=x 因爲克隆對象與原對象不是同一個對象
對任何的對象x,都有x.clone().getClass()= =x.getClass()//克隆對象與原對象的類型一樣
如果對象x的equals()方法定義恰當,那麼x.clone().equals(x)應該成立
對於以上三點要注意,這3項約定並沒有強制執行,所以如果用戶不遵循此約定,那麼將會構造出不正確的克隆對象,所以根據effective java的建議:

謹慎的使用clone方法,或者儘量避免使用。
淺複製實例

對象中全部是基本類型
public class Teacher implements Cloneable{
    private String name;
    private int age;

    public Teacher(String name, int age){
        this.name = name;
        this.age = age;
    }
    // 覆蓋
    @Override
    public Object clone() {
        try {
            return super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
            return null;
        }
    }
    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 class test {
    Teacher origin = new Teacher("tony", 11);
    System.out.println(origin.getName());
    Teacher clone = (Teacher) origin.clone();
    clone.setName("clone");
    System.out.println(origin.getName());
    System.out.println(clone.getName());
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
結果:

tony
tony
clone


從運行結果和圖上可以知道,克隆後的值變量會開闢新的內存地址,克隆對象修改值不會影響原來對象。

對象中含有引用類型
public class Teacher implements Cloneable{
    private String name;
    private int age;
    private Student student;

    public Teacher(String name, int age, Student student){
        this.name = name;
        this.age = age;
        this.student = student;
    }
    // 覆蓋
    @Override
    public Object clone() {
        try {
            return super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
            return null;
        }
    }
    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 Student getStudent() {
        return student;
    }

    public void setStudent(Student student) {
        this.student = student;
    }
}

// 學生類
public class Student {
    private String name;
    private int age;
    public Student(String name, int age){
        this.name = name;
        this.age = age;
    }
     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 class test {
    public static void main(String[] args) {
        Student student = new Student("學生1" ,11);
        Teacher origin = new Teacher("老師", 11, student);;
        Teacher clone = (Teacher) origin.clone();
        System.out.println("比較克隆後的引用對象");
        System.out.println(origin.getStudent().getClass() == clone.getStudent().getClass());
        Student student2 = new Student("學生2", 12);
        clone.setStudent(student2);
        System.out.println("克隆後,比較克隆對象改變引用");
        System.out.println(origin.getStudent().getClass() == clone.getStudent().getClass());
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
運行結果:

比較克隆後的引用對象
true
克隆後,比較克隆對象改變引用
true


如圖可知,引用類型只會存在一份內存地址,執行object的clone方法拷貝的也是引用的複製(這部分的內存空間不一樣,)但是引用指向的內存空間是一樣的,原對象修改引用變量或者淺拷貝對象修改引用變量都會引起雙方的變化

重點:綜上兩個方面可以知道,Object的clone方法是屬於淺拷貝,基本變量類型會複製相同值,而引用變量類型也是會複製相同的引用。

深複製實例

從上面的淺拷貝可以知道,對於引用的變量只會拷貝引用指向的地址,也就是指向同一個內存地址,但是很多情況下我們需要的是下面圖的效果:

深拷貝實現的是對所有可變(沒有被final修飾的引用變量)引用類型的成員變量都開闢內存空間所以一般深拷貝對於淺拷貝來說是比較耗費時間和內存開銷的。

深拷貝的兩種方式:

重寫clone方法實現深拷貝
學生類:

public class Student implements Cloneable {
    private String name;
    private int age;
    public Student(String name, int age){
        this.name = name;
        this.age = age;
    }

    @Override
    protected Object clone()  {
        try {
            return super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
            return null;
        }
    }

    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;
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
老師類:

public class Teacher implements Cloneable{
    private String name;
    private int age;
    private Student student;

    public Teacher(String name, int age, Student student){
        this.name = name;
        this.age = age;
        this.student = student;
    }
    // 覆蓋
    @Override
    public Object clone() {
        Teacher t = null;
        try {
            t = (Teacher) super.clone();
            t.student = (Student)student.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return t;
    }
    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 Student getStudent() {
        return student;
    }

    public void setStudent(Student student) {
        this.student = student;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
測試端:

public class test {
    public static void main(String[] args) {
        Student s = new Student("學生1", 11);
        Teacher origin = new Teacher("老師原對象", 23, s);
        System.out.println("克隆前的學生姓名:" + origin.getStudent().getName());
        Teacher clone = (Teacher) origin.clone();
        // 更改克隆後的學生信息 更改了姓名
        clone.getStudent().setName("我是克隆對象更改後的學生2");
        System.out.println("克隆後的學生姓名:" + clone.getStudent().getName());
    }
}
1
2
3
4
5
6
7
8
9
10
11
運行結果:

克隆前的學生姓名:學生1
克隆後的學生姓名:我是克隆對象更改後的學生2
序列化實現深克隆
我們發現上面通過object的clone方法去實現深克隆十分麻煩, 因此引出了另外一種方式:序列化實現深克隆。

概念:

序列化:把對象寫到流裏
反序列化:把對象從流中讀出來
在Java語言裏深複製一個對象,常常可以先使對象實現Serializable接口,然後把對象(實際上只是對象的一個拷貝)寫到一個流裏,再從流裏讀出來,便可以重建對象。

注意:

寫在流裏的是對象的一個拷貝,而原對象仍然存在於JVM裏面。
對象以及對象內部所有引用到的對象都是可序列化的
如果不想序列化,則需要使用transient來修飾
案例:

Teacher:

public class Teacher implements Serializable{
    private String name;
    private int age;
    private Student student;

    public Teacher(String name, int age, Student student){
        this.name = name;
        this.age = age;
        this.student = student;
    }
    // 深克隆
    public Object deepClone() throws IOException, ClassNotFoundException {
        // 序列化
        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();
    }
    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 Student getStudent() {
        return student;
    }

    public void setStudent(Student student) {
        this.student = student;
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
Student:

public class Student implements Serializable {
    private String name;
    private int age;
    public Student(String name, int age){
        this.name = name;
        this.age = age;
    }

    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;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
client:

public class test {
    public static void main(String[] args) {
        try {
            Student s = new Student("學生1", 11);
            Teacher origin = new Teacher("老師原對象", 23, s);
            System.out.println("克隆前的學生姓名:" + origin.getStudent().getName());
            Teacher clone = (Teacher) origin.deepClone();
            // 更改克隆後的d學生信息 更改了姓名
            clone.getStudent().setName("我是克隆對象更改後的學生2");
            System.out.println("克隆後的學生姓名:" + clone.getStudent().getName());
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
當然這些工作都有現成的輪子了,藉助於Apache Commons可以直接實現:

淺克隆:BeanUtils.cloneBean(Object obj);
深克隆:SerializationUtils.clone(T object);
最後探討

在java中爲什麼實現了Cloneable接口,就可以調用Object中的Clone方法

參考以下回答:

https://www.zhihu.com/question/52490586
————————————————
版權聲明:本文爲CSDN博主「codecarver」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/pulong0748/article/details/85079045

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章