Java學習之深拷貝淺拷貝及對象拷貝的兩種方式

I. Java之Clone

0. 背景

對象拷貝,是一個很是基礎的內容了,爲何會單獨的把這個領出來說解,主要是先前遇到了一個很是有意思的場景javascript

有一個任務,須要解析類xml標記語言,而後生成document對象,以後將會有一系列針對document對象的操做java

經過實際的測試,發現生成Document對象是比較耗時的一個操做,再加上這個任務場景中,須要解析的xml文檔是固定的幾個,那麼一個能夠優化的思路就是能不能緩存住建立後的Document對象,在實際使用的時候clone一份出來c++

1. 內容說明

看到了上面的應用背景,天然而言的就會想到深拷貝了,本篇博文則主要內容以下spring

  • 介紹下兩種拷貝方式的區別
  • 深拷貝的輔助工具類
  • 如何自定義實現對象拷貝

II. 深拷貝和淺拷貝

0. 定義說明

深拷貝apache

至關於建立了一個新的對象,只是這個對象的全部內容,都和被拷貝的對象如出一轍而已,即二者的修改是隔離的,相互之間沒有影響數組

淺拷貝緩存

也是建立了一個對象,可是這個對象的某些內容(好比A)依然是被拷貝對象的,即經過這兩個對象中任意一個修改A,兩個對象的A都會受到影響工具

看到上面兩個簡單的說明,那麼問題來了性能

  • 淺拷貝中,是全部的內容公用呢?仍是某些內容公用?
  • 從隔離來將,都不但願出現淺拷貝這種方式了,太容易出錯了,那麼兩種拷貝方式的應用場景是怎樣的?

1. 淺拷貝

通常來講,淺拷貝方式須要實現Cloneable接口,下面結合一個實例,來看下淺拷貝中哪些是獨立的,哪些是公用的測試

@Data
public class ShallowClone implements Cloneable {

    private String name;

    private int age;

    private List<String> books;


    public ShallowClone clone() {
        ShallowClone clone = null;
        try {
            clone = (ShallowClone) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return clone;
    }


    public static void main(String[] args) {
        ShallowClone shallowClone = new ShallowClone();
        shallowClone.setName("SourceName");
        shallowClone.setAge(28);
        List<String> list = new ArrayList<>();
        list.add("java");
        list.add("c++");
        shallowClone.setBooks(list);


        ShallowClone cloneObj = shallowClone.clone();


        // 判斷兩個對象是否爲同一個對象(便是否是新建立了一個實例)
        System.out.println(shallowClone == cloneObj);

        // 修改一個對象的內容是否會影響另外一個對象
        shallowClone.setName("newName");
        shallowClone.setAge(20);
        shallowClone.getBooks().add("javascript");
        System.out.println("source: " + shallowClone.toString() + "\nclone:" + cloneObj.toString());
        
        shallowClone.setBooks(Arrays.asList("hello"));
        System.out.println("source: " + shallowClone.toString() + "\nclone:" + cloneObj.toString());
    }
}

輸出結果:

false
source: ShallowClone(name=newName, age=20, books=[java, c++, javascript])
clone:ShallowClone(name=SourceName, age=28, books=[java, c++, javascript])
source: ShallowClone(name=newName, age=20, books=[hello])
clone:ShallowClone(name=SourceName, age=28, books=[java, c++, javascript])

結果分析:

  • 拷貝後獲取的是一個獨立的對象,和原對象擁有不一樣的內存地址
  • 基本元素類型,二者是隔離的(雖然上面只給出了int,String)
    • 基本元素類型包括:
    • int, Integer, long, Long, char, Charset, byte,Byte, boolean, Boolean, float,Float, double, Double, String
  • 非基本數據類型(如基本容器,其餘對象等),只是拷貝了一份引用出去了,實際指向的依然是同一份

其實,淺拷貝有個很是簡單的理解方式:

淺拷貝的整個過程就是,建立一個新的對象,而後新對象的每一個值都是由原對象的值,經過 = 進行賦值

這個怎麼理解呢?

上面的流程拆解就是:

- Object clone = new Object();
- clone.a = source.a
- clone.b = source.b
- ...

那麼=賦值有什麼特色呢?

基本數據類型是值賦值;非基本的就是引用賦值

2. 深拷貝

深拷貝,就是要建立一個全新的對象,新的對象內部全部的成員也都是全新的,只是初始化的值已經由被拷貝的對象肯定了而已

那麼上面的實例改爲深拷貝應該是怎樣的呢?

能夠加上這麼一個方法

public ShallowClone deepClone() {
    ShallowClone clone = new ShallowClone();
    clone.name = this.name;
    clone.age = this.age;
    if (this.books != null) {
        clone.books = new ArrayList<>(this.books);
    }
    return clone;
}


// 簡單改一下測試case
public static void main(String[] args) {
    ShallowClone shallowClone = new ShallowClone();
    shallowClone.setName("SourceName");
    shallowClone.setAge(new Integer(1280));
    List<String> list = new ArrayList<>();
    list.add("java");
    list.add("c++");
    shallowClone.setBooks(list);


    ShallowClone cloneObj = shallowClone.deepClone();


    // 判斷兩個對象是否爲同一個對象(便是否是新建立了一個實例)
    System.out.println(shallowClone == cloneObj);

    // 修改一個對象的內容是否會影響另外一個對象
    shallowClone.setName("newName");
    shallowClone.setAge(2000);
    shallowClone.getBooks().add("javascript");
    System.out.println("source: " + shallowClone.toString() + "\nclone:" + cloneObj.toString());


    shallowClone.setBooks(Arrays.asList("hello"));
    System.out.println("source: " + shallowClone.toString() + "\nclone:" + cloneObj.toString());
}

輸出結果爲:

false
source: ShallowClone(name=newName, age=2000, books=[java, c++, javascript])
clone:ShallowClone(name=SourceName, age=1280, books=[java, c++])
source: ShallowClone(name=newName, age=2000, books=[hello])
clone:ShallowClone(name=SourceName, age=1280, books=[java, c++])

結果分析:

  • 深拷貝獨立的對象
  • 拷貝後對象的內容,與原對象的內容徹底不要緊,都是獨立的

簡單來講,深拷貝是須要本身來實現的,對於基本類型能夠直接賦值,而對於對象、容器、數組來說,須要建立一個新的出來,而後從新賦值

3. 應用場景區分

深拷貝的用途咱們很容易能夠想見,某個複雜對象建立比較消耗資源的時候,就能夠緩存一個藍本,後續的操做都是針對深clone後的對象,這樣就不會出現混亂的狀況了

那麼淺拷貝呢?感受留着是一個坑,一我的修改了這個對象的值,結果發現對另外一我的形成了影響,真不是坑爹麼?

假設又這麼一個通知對象長下面這樣

private String notifyUser;

// xxx

private List<String> notifyRules;

咱們如今隨機挑選了一千我的,同時發送通知消息,因此須要建立一千個上面的對象,這些對象中呢,除了notifyUser不一樣,其餘的都同樣

在發送以前,忽然發現要臨時新增一條通知信息,若是是淺拷貝的話,只用在任意一個通知對象的notifyRules中添加一調消息,那麼這一千個對象的通知消息都會變成最新的了;而若是你是用深拷貝,那麼苦逼的得遍歷這一千個對象,每一個都加一條消息了


III. 對象拷貝工具

上面說到,淺拷貝,須要實現Clonebale接口,深拷貝通常須要本身來實現,那麼我如今拿到一個對象A,它本身沒有提供深拷貝接口,咱們除了主動一條一條的幫它實現以外,有什麼輔助工具可用麼?

對象拷貝區別與clone,它能夠支持兩個不一樣對象之間實現內容拷貝

Apache的兩個版本:(反射機制)

org.apache.commons.beanutils.PropertyUtils.copyProperties(Object dest, Object orig)


org.apache.commons.beanutils.BeanUtils#cloneBean

Spring版本:(反射機制)

org.springframework.beans.BeanUtils.copyProperties(Object source, Object target, Class editable, String[] ignoreProperties)

cglib版本:(使用動態代理,效率高)

net.sf.cglib.beans.BeanCopier.copy(Object paramObject1, Object paramObject2, Converter paramConverter)

從上面的幾個有名的工具類來看,提供了兩種使用者姿式,一個是反射,一個是動態代理,下面分別來看兩種思路

1. 藉助反射實現對象拷貝

經過反射的方式實現對象拷貝的思路仍是比較清晰的,先經過反射獲取對象的全部屬性,而後修改可訪問級別,而後賦值;再獲取繼承的父類的屬性,一樣利用反射進行賦值

上面的幾個開源工具,內部實現封裝得比較好,因此直接貼源碼可能不太容易一眼就能看出反射方式的原理,因此簡單的實現了一個, 僅提供思路

public static void copy(Object source, Object dest) throws Exception {
    Class destClz = dest.getClass();

    // 獲取目標的全部成員
    Field[] destFields = destClz.getDeclaredFields();
    Object value;
    for (Field field : destFields) { // 遍歷全部的成員,並賦值
        // 獲取value值
        value = getVal(field.getName(), source);

        field.setAccessible(true);
        field.set(dest, value);
    }
}


private static Object getVal(String name, Object obj) throws Exception {
    try {
        // 優先獲取obj中同名的成員變量
        Field field = obj.getClass().getDeclaredField(name);
        field.setAccessible(true);
        return field.get(obj);
    } catch (NoSuchFieldException e) {
        // 表示沒有同名的變量
    }

    // 獲取對應的 getXxx() 或者 isXxx() 方法
    name = name.substring(0, 1).toUpperCase() + name.substring(1);
    String methodName = "get" + name;
    String methodName2 = "is" + name;
    Method[] methods = obj.getClass().getMethods();
    for (Method method : methods) {
        // 只獲取無參的方法
        if (method.getParameterCount() > 0) {
            continue;
        }

        if (method.getName().equals(methodName)
                || method.getName().equals(methodName2)) {
            return method.invoke(obj);
        }
    }

    return null;
}

上面的實現步驟仍是很是清晰的,首先是找同名的屬性,而後利用反射獲取對應的值

Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
return field.get(obj);

若是找不到,則找getXXX, isXXX來獲取

2. 代理的方式實現對象拷貝

Cglib的BeanCopier就是經過代理的方式實現拷貝,性能優於反射的方式,特別是在大量的數據拷貝時,比較明顯

代理,咱們知道能夠區分爲靜態代理和動態代理,簡單來說就是你要操做對象A,可是你不直接去操做A,而是找一箇中轉porxyA, 讓它來幫你操做對象A

那麼這種技術是如何使用在對象拷貝的呢?

咱們知道,效率最高的對象拷貝方式就是Getter/Setter方法了,前面說的代理的含義指咱們不直接操做,而是找個中間商來賺差價,那麼方案就出來了

將原SourceA拷貝到目標DestB

  • 建立一個代理 copyProxy
  • 在代理中,依次調用 SourceA的get方法獲取屬性值,而後調用DestB的set方法進行賦值

實際上BeanCopier的思路大體如上,具體的方案固然就不太同樣了, 簡單看了一下實現邏輯,挺有意思的一塊,先留個坑,後面單獨開個博文補上

說明

從實現原理和經過簡單的測試,發現BeanCopier是掃描原對象的getXXX方法,而後賦值給同名的 setXXX 方法,也就是說,若是這個對象中某個屬性沒有get/set方法,那麼就沒法賦值成功了


IV. 小結

1. 深拷貝和淺拷貝

深拷貝

至關於建立了一個新的對象,只是這個對象的全部內容,都和被拷貝的對象如出一轍而已,即二者的修改是隔離的,相互之間沒有影響

  • 徹底獨立

淺拷貝

也是建立了一個對象,可是這個對象的某些內容(好比A)依然是被拷貝對象的,即經過這兩個對象中任意一個修改A,兩個對象的A都會受到影響

  • 等同與新建立一個對象,而後使用=,將原對象的屬性賦值給新對象的屬性
  • 須要實現Cloneable接口

2. 對象拷貝的兩種方法

經過反射方式實現對象拷貝

主要原理就是經過反射獲取全部的屬性,而後反射更改屬性的內容

經過代理實現對象拷貝

將原SourceA拷貝到目標DestB

建立一個代理 copyProxy 在代理中,依次調用 SourceA的get方法獲取屬性值,而後調用DestB的set方法進行賦值

V. 其餘

聲明

盡信書則不如,已上內容,純屬一家之言,因本人能力通常,看法不全,若有問題,歡迎批評指正

掃描關注,java分享

QrCode

相關文章
相關標籤/搜索