Clone 原型模式 | 解說實現 | Android Framework Intent

Clone 原型模式 | 解說實現 | Android Framework Intent

Overview of Content

如有引用參考請詳註出處,感謝

在這篇文章中,我們將深入研究複製(Clone Pattern)的應用場景

首先,我們會提供整體內容的概述,並進入複製的使用場景。接著,我們將解釋複製的定義,並透過UML結構呈現其模型。在深入研究時,我們會探討複製的特色,並特別注意其中的注意事項。同時,我們會分析複製技術的優缺點,使讀者對其應用有更清晰的認識。

在實際實現部分,我們將深入探討淺拷貝和深拷貝的區別,並以 Java 中的 LinkedList、ArrayList 以及 Arrays.copyOf 方法為例進行淺拷貝的實踐

最後,我們將探討在 Android 源碼中的 Intent 傳遞中複製的應用場景。這將有助於讀者更全面地理解複製的實際應用。


Clone 使用場景

原型模式 多是使用在,物件複雜初始化的地方,複製一個已經存在的實例,可使效率提高

● 在類別初始化時消耗過多的資源,原型拷貝可降低消耗量

● 透過 new 出的一個物件,需要反鎖的過程

以下創立物件的過程

  1. 檢查 Classloader:加載是否已經加載該 Class
  2. 分配記憶體內存:避免 Mutli Thread 不安全的使用記憶體
  3. 內存初始化:清理舊資料
  4. 設定 - 對象頭資訊
  5. 對象初始化:初始化 static Field、呼叫建後函數

● 一個物件需要給其他類別修改值時,最終才以一個結果決定值時,可以拷貝多個物件給需要的類,這可稱為 保護性拷貝

要用 Clone 還是 new 來創建對象 ?

Clone 並不依定比 new 速度還要快(一般來說是會比 new 快),需要使用者自己考量物件的大小、方便性、使用場景... 等等

Clone 是透過記憶體二進制流(Binary stream)拷貝


Clone 定義 & Clone UML

Clone 定義

用原型實例指定創建對象的種類,並且通過 拷貝原型對象取得新對象

Clone UML 模型

角色功能
Cloneable標示介面,這種介面內部通常沒有方法,是為了標示該類「擁有」某種特性
ConcreateClone實作 Cloneable 介面的類,也就是真正要拷貝時會調用到的類

Clone 特色 - 注意事項

A. 不會呼叫建構函數

使用 clone 方法 不會呼叫建構函數

B. 預設淺拷貝clone不會複製內部已有的物件(如 ListMap... 等等其他物件),所以要 手動 針對內部有的物件進行操作 (深拷貝)

淺拷貝只會拷貝,基礎的 8 大類別、String 類... 其他有關 數組、引用對象 是不會拷貝的

● 在 Java 中 Cloneable 是一個 標示接口,這個接口內並沒有任何方法


// Cloneable.java

public interface Cloneable {
}

所謂的 標示,代表該接口是用來 判斷,像是所有類都隱式繼承 Object 類,Object 內就有一個 clone 方法,在 clone 方法內就有判斷該接口


// Object.java

protected Object clone() throws CloneNotSupportedException {
    // 判斷該類是否有 Cloneable 接口
    if (!(this instanceof Cloneable)) {
        throw new CloneNotSupportedException("Class " + getClass().getName() +
                                             " doesn't implement Cloneable");
    }

    // 透過 Native 複製物件
    return internalClone();
}

/*
 * Native helper method for cloning.
 */
@FastNative
private native Object internalClone();

Clone - 優缺點

拷貝物件不一定比 new 物件速度更快,需要多加考量,原型模式是在 記憶體 中二進制串流(Binary Stream)的拷貝比起 new 可少掉許多步驟

優點缺點
唯讀物件可以使用,防止物件被修改它不進行 new 的流程,也就不呼叫建構函數
特別是在一個需要產生大量物件的地方,可以看出 clone 的好處優點是少了約束,缺點也是少了約束

Clone 實現:淺拷貝 & 深拷貝

如果有寫過 C++,就會對 C++ 深淺拷貝 比較有印象,因為必須覆寫 指定操作符複製建構函數 這兩個方法

ShallowClone 淺拷貝

● 預設物件的 clone 是淺拷貝,我們先來寫一個簡單的範例


public class ShallowClone implements Cloneable {

    String name;
    String describe;

    @Override
    public ShallowClone clone() {
        System.out.println("ShallowClone Use clone");
        try {
            // clone 是淺拷貝
            return (ShallowClone) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return null;
    }

    ShallowClone() {
        //"1. " 原先建構函數
        System.out.println("Make ShallowClone Store");

        name = "Java";
        describe = "Basic";
    }

    public void print() {
        System.out.println(
                "This hashCode: " +  hashCode() +
                "\nName: " + name + ", " + name.hashCode() +
                "\nDescribe:" + describe + ", " + describe.hashCode());
    }
}



class ShallowMain {

    public static void main(String[] args) {
        System.out.println("\nOriginal----------------Create original Object:");
        ShallowClone original = new ShallowClone();
        original.print();

        System.out.println("\nClone----------------Start clone:");
        ShallowClone clone = original.clone();
        clone.print();

        System.out.println("\nClone----------------Change clone member:");
        original.name = "Android";
        original.describe = "OOP";
        clone.print();

        System.out.println("\nOriginal----------------After clone change, original Object:");
        original.print();
    }
}

--實作--

A. 產生不同物件:透過 Object#clone 函數,確實自身可以產生不同的物件 (透過 hashCode 觀察)、其 member 也可以

● 請注意!透過 Object#clone 並 不會觸發建構函數 constructor

B. 複製原物建 member value:Clone 出來的物件,其 member 會的 Value 都與原來物件指向的位置相同 (這樣也滿符合 clone 的意思)

C. 自己手動設定 Clone 物件的成員,會發現 原本指向元物件的位置會改變,變成全新的物件位置

● 這有點類似於 Linux 的 Copy on Write 技術

DeepClone 深拷貝

● 深拷貝就是我們要手動處理當前物件 member 的 clone 行為,新增一個 ArrayList 進行拷貝

A. 首先先來看看,未處理引用 member 時,就拷貝的狀況


public class DeepClone implements Cloneable {

    String name;
    String describe;

    List<Integer> number = new ArrayList<>();

    @Override
    public DeepClone clone() {
        System.out.println("DeepClone Use clone");
        try {
            return (DeepClone) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return null;
    }

    DeepClone() {
        //"1. " 原先建構函數
        System.out.println("Make DeepClone Store");

        name = "Java";
        describe = "Basic";
        number.add(1111);
    }

    public void print() {
        System.out.println(
                "This hashCode: " +  hashCode() +
                        "\nName: " + name + ", " + name.hashCode() +
                        "\nDescribe: " + describe + ", " + describe.hashCode() +
                        "\nNumber: " + number.toString()
                );
    }
}

class DeepMain {

    public static void main(String[] args) {
        System.out.println("\nOriginal----------------Create original Object:");
        DeepClone original = new DeepClone();
        original.print();

        System.out.println("\nClone----------------Start clone:");
        DeepClone clone = original.clone();
        clone.print();

        System.out.println("\nClone----------------Change clone member:");
        clone.name = "Android";
        clone.describe = "OOP";
        clone.number.add(9999);
        clone.print();

        System.out.println("\nOriginal----------------After clone change, original Object:");
        original.print();
    }
}

從結果可以看出一個重要問題,LinkedList 這個 member 被重複使用。在 clone 後,添加 List 會 影響到原先的物件

--實作--

B. 解決方式:自己手動處理 clone 方法,解決每個 member 的 clone 問題


    @Override
    public DeepClone clone() {
        System.out.println("DeepClone Use clone");

        try {
            DeepClone clone = (DeepClone) super.clone();

            clone.name = clone.describe = "";
            clone.number = new ArrayList<>(this.number);

            return clone;
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return null;
    }

--實作--

Java List 列表

LinkedList 淺拷貝

● Java LinkedList 類,透過自己處理 clone 方法來達成拷貝,我們現在來看看它的 Clone 方式


public class LinkedListClone implements Cloneable {

    private String name;
    private int age;

    private final LinkedList<String> event = new LinkedList<>();

    @Override
    public LinkedListClone clone() {
        System.out.println("\n\n LinkedListClone Use clone");
        try {
            // clone 是淺拷貝
            return (LinkedListClone) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return null;
    }

    LinkedListClone() {
        //"1. " 原先建構函數
        System.out.println("Make LinkedListClone Store");
    }

    public void setName(String name) {
        this.name = name;
    }

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

    public void setEvent(String... mem) {
        for(String m:mem) {
            if(!event.contains(m)) {
                event.add(m);
            }
        }
    }

    public void print() {
        System.out.println("Name: " + name);
        System.out.println("Age: " + age);

        for(String m: event) {
            System.out.print(m + "... ");
        }
        System.out.println();
    }
}

class LinkedListMain {
    public static void main(String[] args) {

        LinkedListClone i = new LinkedListClone();
        i.setName("Pan");
        i.setAge(13);
        i.setEvent("Hello", "World");
        i.print();

        LinkedListClone ii = i.clone();
        ii.setAge(16);
        ii.setEvent("Test", "1234");
        ii.print();
    }

--實作--

● LinkedList 在 clone 後:將原來 List 的內容,添加到新的 List 中 (LinkedList 有自己處理 clone 方法),讓 clone 後的新物件擁有相同的內容

● 新 LinkedList 加入的成員與舊 LinkedList 相同物件 (HashCode 相同)


// LinkedList.java

    private LinkedList<E> superClone() {
        try {
            return (LinkedList)super.clone();
        } catch (CloneNotSupportedException var2) {
            throw new InternalError(var2);
        }
    }

    public Object clone() {
        LinkedList<E> clone = this.superClone();

        clone.first = clone.last = null;
        clone.size = 0;
        clone.modCount = 0;

        // 在新的 LinkedList 中添加原來物件的內容
        for(Node<E> x = this.first; x != null; x = x.next) {
            clone.add(x.item);
        }

        return clone;
    }

ArrayList 淺拷貝

● 像是 ArrayList 類就有 Cloneable 界面標示


// ArrayList.java

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{

    ... 省略其他方法

    public Object clone() {
        try {
            ArrayList<?> v = (ArrayList<?>) super.clone();
            // Arrays.copyOf 是淺拷貝
            v.elementData = Arrays.copyOf(elementData, size);
            v.modCount = 0;
            
            return v;
        } catch (CloneNotSupportedException e) {
            // this shouldn't happen, since we are Cloneable
            throw new InternalError(e);
        }
    }
}

● 使用 Arrays.copyOf 方法,其實內部是使用 Array.newInstance 反射創建對象,再使用 System.arraycopy() 複製物件,但是 仍然為淺拷貝


// Arrays.java

    public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
        T[] copy = ((Object)newType == (Object)Object[].class)
            ? (T[]) new Object[newLength]
            : (T[]) Array.newInstance(newType.getComponentType(), newLength);
        
        // 將資料複製到 新 Array 中
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        return copy;
    }

Arrays.copyOf 方法 - 淺拷貝

● 當 Arrays.copyOf 使用在數組對象時,改變數組就會改變原來的對象,代表其實 Arrays.copyOf 只是拷貝了數組的位置 (如果修改物件內容,就會直接改到原來的物件)


import java.util.ArrayList;
import java.util.Arrays;

public class ArrayCopy_Test {
    
    public static void main(String[] args) {
        Info[] info = {new Info(9999), new Info(6666)};
        Info[] copyInfo = Arrays.copyOf(info, info.length);

        System.out.println("Origin ---------------------------");
        showInfo(copyInfo);

        // 直接修改 copyOf 後的物件
        copyInfo[0].mId = 3333;

        System.out.println("Clone ---------------------------");
        showInfo(copyInfo);

        System.out.println("Origin --------------------------- After clone change");
        showInfo(info);
    }

    private static void showInfo(Info...infos) {
        for(int i = 0; i < infos.length; i++) {
            if(infos[i] == null) {
                System.out.println("null");
                continue;
            }
            System.out.println(infos[i].toString());
        }
        System.out.println();
    }

}

class Info {

    static class Stamp {
        String signature;
    }

    long mId;
    Stamp stamp;

    Info(long id) {
        mId = id;
    }

    public Info createStamp(String s) {
        stamp = new Stamp();
        stamp.signature = s;
        return this;
    }


    @Override
    public String toString() {
        String base = "id = " + mId;
        if(stamp != null) {
            base += (", Stamp = " + stamp.signature);
        }

        return base;
    }
}

以下範例,使用 ArrayList#cloneArrays.copyOf 操作的結果

● 從上面兩個範例裡面可以看到它改變了兩次,並且都是透過 改變 copy 對象的對象就可以影響到原來的對象,也就證明了 Arrays.copyOf 是複製了對象的地址

--實作結果--

Android Soucre Intent

Intent 傳遞

● Intent 是四大組件傳遞資料的載體 (Activity、Service、Broadcast ...),正是因為有 Intent 才能解開四大組件的偶合 (弱耦合)


private void testIntentClone() {

    Intent intent = new Intent();
    intent.setData(Uri.parse("https:www.google.com"));
    intent.putExtra("GOOGLE_VERSION", 33);
    Log.e("TEST123", "\nOriginal: \nData: " + intent.getData() +
            "\nGOOGLE_VERSION: " + intent.getIntExtra("GOOGLE_VERSION", -1));

    Intent cloneOne = new Intent(intent);
    Log.e("TEST123", "\n Clone: \nData: " + cloneOne.getData() +
            "\nGOOGLE_VERSION: " + intent.getIntExtra("GOOGLE_VERSION", -1));
}

從結果來看 2 個 Intent 相同

● Intent 類的 clone 方法會發現 它並沒有呼叫 super.clone 方法,因為開發者判斷使用 new 成本較低,從這裡可以看出 對象拷貝的方案是依照建構成本來考量


// Intent.java

    @Override
    public Object clone() {
        // 使用建構函數複製
        return new Intent(this);
    }

    /**
     * Copy constructor.
     */
    public Intent(Intent o) {
        this.mAction = o.mAction;
        this.mData = o.mData;
        this.mType = o.mType;
        this.mPackage = o.mPackage;
        this.mComponent = o.mComponent;
        this.mFlags = o.mFlags;
        this.mContentUserHint = o.mContentUserHint;
        if (o.mCategories != null) {
            this.mCategories = new ArraySet<String>(o.mCategories);
        }
        if (o.mExtras != null) {
            this.mExtras = new Bundle(o.mExtras);
        }
        if (o.mSourceBounds != null) {
            this.mSourceBounds = new Rect(o.mSourceBounds);
        }
        if (o.mSelector != null) {
            this.mSelector = new Intent(o.mSelector);
        }
        if (o.mClipData != null) {
            this.mClipData = new ClipData(o.mClipData);
        }
    }

更多的物件導向設計

物件導向的設計基礎如下,如果是初學者或是不熟悉的各位,建議可以從這些基礎開始認識,打好基底才能走個更穩(在學習的時候也需要不斷回頭看)!

設計建模 2 大概念- UML 分類、使用

物件導向設計原則 – 6 大原則(一)

物件導向設計原則 – 6 大原則(二)

創建、行為、結構型設計 8 個比較 | 包裝模式 | 最佳實踐

創建模式 Creation Patterns

創建模式 PK

創建模式 - Creation Patterns

結構模式 Structural Patterns

結構模式 PK

結構模式 - Structural Patterns

結構模式專注於「物件之間的組成」,以形成更大的結構。這些模式可以幫助你確保當系統進行擴展或修改時,不會破壞其整體結構。例如,外觀模式、代理模式… 等等,詳細解說請點擊以下連結

Bridge 橋接模式 | 解說實現 | 物件導向設計

Decorate 裝飾模式 | 解說實現 | 物件導向設計

Proxy 代理模式 | 解說實現 | 分析動態代理

Iterator 迭代設計 | 解說實現 | 物件導向設計

Facade 外觀、門面模式 | 解說實現 | 物件導向設計

Adapter 設計模式 | 解說實現 | 物件導向設計

Leave a Comment

Comments

No comments yet. Why don’t you start the discussion?

發表迴響