Java&Android 基礎知識梳理(11) - 淺拷貝 Vs 深拷貝

澤毛發表於2018-01-30

一、基本資料型別 & 引用型別

1.1 基本概念

在討論 淺拷貝 & 深拷貝 這個問題之前,我們需要先了解 基本資料型別 & 引用型別 這兩者之間的區別,否則後面會很疑惑。在Java當中,這兩類的代表分別為:

  • 八種 基本資料型別:byteshortintlongfloatdoublecharboolean
  • 引用型別:除去基本資料型別的其它型別都是引用資料型別,例如類、介面、陣列。

(1) JAVA 基本資料型別與引用資料型別 一文中總結了這兩者的區別:

基本資料型別 引用資料型別
變數名指向具體的數值 變數名指向存資料物件的記憶體地址,即變數名指向hash
變數在宣告之後就會立刻分配給他記憶體空間 它以特殊的方式指向物件實體,這類變數宣告時不會分配記憶體,只是儲存了一個記憶體地址
基本型別之間的賦值是建立新的拷貝 物件之間的賦值只是傳遞引用
“==”和“!=”是在比較值 “==”“!=”是在比較兩個引用是否相同
使用時需要賦具體值,判斷時使用== 使用時可以賦值null,判斷時使用equals方法

1.2 基本資料型別 和 引用資料型別 傳遞區別

程式設計語言中有關引數傳遞給方法的兩個專業術語是:

  • 按值呼叫:表示方法接收的是呼叫者提供的值。
  • 按引用呼叫:表示方法接收的是呼叫者提供的變數的地址。

Java不存在按引用呼叫,也就是說,假如方法傳遞的是一個引用資料型別,那麼可以修改 引用所指向的物件的屬性,但不能 讓引用指向其它的物件

1.2.1 傳遞基本資料型別

    public static void methodBasic() {
        int lNum = 3;
        methodRunInner(lNum);
        Log.d("CopySample", "After methodRunInner, lNum=" + lNum);
    }

    private static void methodRunInner(int lNum) {
        lNum++;
        Log.d("CopySample", "methodRunInner, lNum=" + lNum);
    }
複製程式碼

傳遞基本資料型別

1.2.2 傳遞引用資料型別

當傳遞的是引用資料型別,可以在函式中修改該引用所指向的物件的成員變數的值,如下所示:

    public static void methodRef() {
        NumHolder holder = new NumHolder();
        holder.num = 3;
        methodRunInner(holder);
        Log.d("CopySample", "After methodRunInner, holder.num=" + holder.num);
    }

    private static void methodRunInner(NumHolder holder) {
        holder.num++;
        Log.d("CopySample", "methodRunInner, holder.num=" + holder.num);
    }

    private static class NumHolder {
        int num;
    }
複製程式碼

傳遞引用資料型別
但是,我們並不能讓引用指向其它的物件,例子如下所示:

    public static void methodSwapRef() {
        NumHolder lHolder = new NumHolder();
        NumHolder rHolder = new NumHolder();
        lHolder.num = 3;
        rHolder.num = 4;
        methodRunInner(lHolder, rHolder);
        Log.d("CopySample", "methodSwapRef, lHolder.num=" + lHolder.num  + ", rHolder.num=" + rHolder.num);
    }

    private static void methodRunInner(NumHolder lHolder, NumHolder rHolder) {
        NumHolder temp = lHolder;
        lHolder = rHolder;
        rHolder = temp;
        Log.d("CopySample", "methodRunInner, lHolder.num=" + lHolder.num  + ", rHolder.num=" + rHolder.num);
    }

    private static class NumHolder {
        int num;
    }
複製程式碼

在方法中交換引用

二、淺拷貝 Vs 深拷貝

在對基本資料型別和引用資料型別瞭解之後,我們就可以開始分析淺拷貝 & 深拷貝了。

2.1 定義

首先讓我們來看一下它們倆的定義:

  • 淺拷貝:使用一個已知例項對新建立例項的成員變數逐個 賦值,這個方式被稱為淺拷貝。
  • 深拷貝:當一個類的拷貝構造方法,不僅要複製物件的所有非引用成員變數值,還要為引用型別的成員變數建立新的例項,並且初始化為形式引數例項值。

2.2 賦值操作符 =

上面我們講到了一個關鍵詞 - 賦值,那麼讓我們來先看一下賦值操作符=在基本資料型別和引用資料型別上會發生什麼。

2.2.1 基本資料型別

    public static void startRun1() {
        int lNumber = 2;
        int rNumber = lNumber;
        rNumber = 3;
        Log.d("CopySample", "lNumber=" + lNumber + ",rNumber=" + rNumber);
    }
複製程式碼

執行結果為:

基本資料型別 - 賦值結果
對於基本資料型別,當我們用rNumberlNumber進行賦值的時候,實際上是開闢了一塊新的記憶體空間,因此對於rNumber的改變,並不會影響lNumber

2.2.2 引用資料型別

    public static void startRun2() {
        People lPeople = new People();
        lPeople.age = 10;
        People rPeople = lPeople;
        rPeople.age = 20;
        Log.d("CopySample", "lPeople=" + lPeople + ",lPeople.age=" + lPeople.age + ",\n" +
            "rPeople=" + rPeople + ",rPeople.age=" + rPeople.age);
    }

    public static class People {
        int age;
    }
複製程式碼

引用資料型別 - 賦值結果
對於引用資料型別,它們的內容其實是記憶體的地址。因此使用賦值操作符=後,lPeoplerPeople指向同一塊記憶體地址(即上圖中的@f7a53fd),通過該地址我們可以訪問到它儲存的物件。

所以,當我們通過rPeople來改變成員變數age的值之後,通過lPeople訪問該成員變數可以看到更新後的值,這就是 基本資料型別和引用資料型別賦值的區別

2.3 淺拷貝

淺拷貝的前提是該類了實現Cloneable介面,並重寫clone方法。在拷貝某個物件時,呼叫該物件的clone方法返回一個新的物件,該物件就是淺拷貝的結果。

    public static void startRun3() {
        People lPeople = new People();
        lPeople.age = 10;
        lPeople.holder = new People.Holder();
        lPeople.holder.holderValue = 10;
        People rPeople = (People) lPeople.clone();
        rPeople.age = 20;
        rPeople.holder.holderValue = 20;
        Log.d("CopySample", "lPeople=" + lPeople + ",lPeople.age=" + lPeople.age + ",lPeople.holder=" + lPeople.holder + ",\n" +
                "rPeople=" + rPeople + ",rPeople.age=" + rPeople.age + ",rPeople.holder=" + rPeople.holder);
        Log.d("CopySample", "lHolderValue=" + lPeople.holder.holderValue + ",rHolderValue=" + rPeople.holder.holderValue);
    }

    public static class People implements Cloneable {

        int age;
        Holder holder;

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

        public static class Holder {
            int holderValue;
        }
    }
複製程式碼

淺拷貝
從上面的程式碼當中,我們可以看到,淺拷貝與賦值的區別在於:淺拷貝會為拷貝的物件 開闢一塊新的記憶體空間(被拷貝的物件在@f7a53fd,拷貝的物件在@3a8bb43),但是這並不意味著這兩者之間一定是完全獨立的,因為clone方法的預設實現,對類中不同型別的成員變數會有不同的表現。

  • 基本資料型別:對該成員變數進行復制。
  • 引用資料型別:複製引用,但不會開闢新的記憶體空間,因此被拷貝物件的該成員變數與拷貝物件對應的成員變數 指向同一塊記憶體地址(上圖中的@f5b08f2),就和我們在2.2.2中看到的一樣。

正是由於這一區別,當我們通過rPeople對其成員變數進行修改時,lPeopleage屬性(基本資料型別)不受影響,而holder.holderValue(引用資料型別)卻會跟著改變。

2.4 深拷貝

下面,我們先來演示如何對2.2中的例子進行改進,實現深拷貝。我們讓Holder類也實現了Clonable介面,因此呼叫它的clone方法後會返回一個新物件的引用;之後,還要重寫Peopleclone方法,對clone之後的物件中的成員變數holder採用clone方法進行拷貝。

   public static void startRun3() {
        People lPeople = new People();
        lPeople.age = 10;
        lPeople.holder = new People.Holder();
        lPeople.holder.holderValue = 10;
        People rPeople = (People) lPeople.clone();
        rPeople.age = 20;
        rPeople.holder.holderValue = 20;
        Log.d("CopySample", "lPeople=" + lPeople + ",lPeople.age=" + lPeople.age + ",lPeople.holder=" + lPeople.holder + ",\n" +
                "rPeople=" + rPeople + ",rPeople.age=" + rPeople.age + ",rPeople.holder=" + rPeople.holder);
        Log.d("CopySample", "lHolderValue=" + lPeople.holder.holderValue + ",rHolderValue=" + rPeople.holder.holderValue);
    }

    public static class People implements Cloneable {

        int age;
        Holder holder;

        @Override
        protected Object clone() {
            try {
                People people = (People) super.clone();
                people.holder = (People.Holder) this.holder.clone();
                return people;
            } catch (CloneNotSupportedException e) {
                e.printStackTrace();
            }
            return null;
        }

        public static class Holder implements Cloneable {

            int holderValue;

            @Override
            protected Object clone() {
                try {
                    return super.clone();
                } catch (CloneNotSupportedException e) {
                    e.printStackTrace();
                }
                return null;
            }
        }
    }
複製程式碼

深拷貝
從列印的結果可以看到rPeopleholder引用指向了一塊新的記憶體地址@b855c0,因此對它的holderValue進行改變並不會影響到lPeopleholder變數中的holderValue

三、陣列 & 集合的拷貝

3.1 陣列的拷貝

陣列除了預設實現了clone()方法之外,還提供了Arrays.copyOf方法用於拷貝,這兩者都是淺拷貝。

3.1.1 基本資料型別陣列

    public static void startRun4() {
        int[] lNumbers1 = new int[5];
        int[] rNumbers1 = lNumbers1;
        rNumbers1[0] = 1;
        Log.d("CopySample", "lNumbers1[0]=" + lNumbers1[0] + ",rNumbers1[0]=" + rNumbers1[0]);

        int[] lNumbers2 = new int[5];
        int[] rNumbers2 = Arrays.copyOf(lNumbers2, lNumbers2.length);
        rNumbers2[0] = 1;
        Log.d("CopySample", "lNumbers2[0]=" + lNumbers2[0] + ",rNumbers2[0]=" + rNumbers2[0]);

        int[] lNumbers3 = new int[5];
        int[] rNumbers3 = lNumbers3.clone();
        rNumbers3[0] = 1;
        Log.d("CopySample", "lNumbers3[0]=" + lNumbers3[0] + ",rNumbers3[0]=" + rNumbers3[0]);
    }
複製程式碼

執行結果:

基本資料型別陣列

3.1.2 引用資料型別陣列

    public static void startRun5() {
        People[] lNumbers1 = new People[5];
        lNumbers1[0] = new People();
        People[] rNumbers1 = lNumbers1;
        Log.d("CopySample", "lNumbers1[0]=" + lNumbers1[0] + ",rNumbers1[0]=" + rNumbers1[0]);

        People[] lNumbers2 = new People[5];
        lNumbers2[0] = new People();
        People[] rNumbers2 = Arrays.copyOf(lNumbers2, lNumbers2.length);
        Log.d("CopySample", "lNumbers2[0]=" + lNumbers2[0] + ",rNumbers2[0]=" + rNumbers2[0]);

        People[] lNumbers3 = new People[5];
        lNumbers3[0] = new People();
        People[] rNumbers3 = lNumbers3.clone();
        Log.d("CopySample", "lNumbers3[0]=" + lNumbers3[0] + ",rNumbers3[0]=" + rNumbers3[0]);
    }

    public static class People implements Cloneable {

        int age;
        Holder holder;

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

        public static class Holder {
            int holderValue;
        }
    }
複製程式碼

引用資料型別陣列

3.2 集合的拷貝

集合的拷貝也是我們平時經常會遇到的,一般情況下,我們都是用淺拷貝來實現,即通過建構函式或者clone方法。

3.2.1 建構函式和 clone() 預設都是淺拷貝

    public static void listShallowRun() {
        ArrayList<People> lPeoples = new ArrayList<>();
        People people1 = new People();
        lPeoples.add(people1);
        Log.d("CopySample", "listShallowRun, lPeoples[0]=" + lPeoples.get(0));
        ArrayList<People> rPeoples = (ArrayList<People>) lPeoples.clone();
        Log.d("CopySample", "listShallowRun, rPeoples[0]=" + rPeoples.get(0));
    }

    public static class People implements Cloneable {

        int age;
        Holder holder;

        @Override
        protected Object clone() {
            try {
                People people = (People) super.clone();
                people.holder = (People.Holder) this.holder.clone();
                return people;
            } catch (CloneNotSupportedException e) {
                e.printStackTrace();
            }
            return null;
        }

        public static class Holder implements Cloneable {

            int holderValue;

            @Override
            protected Object clone() {
                try {
                    return super.clone();
                } catch (CloneNotSupportedException e) {
                    e.printStackTrace();
                }
                return null;
            }
        }
    }
複製程式碼

集合的淺拷貝

3.2.2 實現集合的深拷貝

在某些特殊情況下,如果需要實現集合的深拷貝,那就要建立一個新的集合,然後通過深拷貝原先集合中的每個元素,將這些元素加入到新的集合當中。

   public static void listDeepRun() {
        ArrayList<People> lPeoples = new ArrayList<>();
        People people1 = new People();
        people1.holder = new People.Holder();
        lPeoples.add(people1);
        Log.d("CopySample", "listShallowRun, lPeoples[0]=" + lPeoples.get(0));
        ArrayList<People> rPeoples = new ArrayList<>();
        for (People people : lPeoples) {
            rPeoples.add((People) people.clone());
        }
        Log.d("CopySample", "listShallowRun, rPeoples[0]=" + rPeoples.get(0));
    }

    public static class People implements Cloneable {

        int age;
        Holder holder;

        @Override
        protected Object clone() {
            try {
                People people = (People) super.clone();
                people.holder = (People.Holder) this.holder.clone();
                return people;
            } catch (CloneNotSupportedException e) {
                e.printStackTrace();
            }
            return null;
        }

        public static class Holder implements Cloneable {

            int holderValue;

            @Override
            protected Object clone() {
                try {
                    return super.clone();
                } catch (CloneNotSupportedException e) {
                    e.printStackTrace();
                }
                return null;
            }
        }
    }
複製程式碼

採用 clone 實現集合的深拷貝

四、參考文獻

(1) Java 基本資料型別與引用資料型別
(2) Java 基本資料型別傳遞與引用傳遞區別詳解
(3) 詳解 Java 中的 clone 方法 -- 原型模式
(4) Java List 複製:淺拷貝與深拷貝
(5) 漸析 Java 的淺拷貝和深拷貝

相關文章