理解深淺拷貝,Immutable,保護(hù)性拷貝

一、對(duì)象拷貝

我們使用 = 的時(shí)候,其實(shí)是引用的拷貝. 多個(gè)引用指向的其實(shí)是同一個(gè)對(duì)象.
上面的例子中 ArrayList<Integer> list = new ArrayList<>()在堆內(nèi)存中創(chuàng)建了ArrayList對(duì)象并且把list引用指向該對(duì)象的地址.
ArrayList<Integer> list2 = list 把list的引用賦值給list2, 兩個(gè)引用指向的都是上一步創(chuàng)建的對(duì)象.
對(duì)象拷貝分為深淺拷貝.

深淺拷貝的概念

對(duì)于基本類型來(lái)說(shuō)拷貝只是值傳遞, 拷貝后的對(duì)象和原對(duì)象的基本類型變量是相互獨(dú)立的. 以下只討論引用類型的情況.

  • 淺拷貝
    如果是引用類型,拷貝的是引用類型的地址值, 也就是和原對(duì)象的引用指向相同的一塊內(nèi)存區(qū)域. 這時(shí)候如果對(duì)象發(fā)生更改. 拷貝對(duì)象和原對(duì)象都會(huì)受到影響.
  • 深拷貝
    深拷貝將具有原始對(duì)象的所有字段的精確復(fù)制,就像淺復(fù)制一樣。但是,另外,如果原始對(duì)象有任何對(duì)其他對(duì)象的引用作為字段,那么也可以通過(guò)調(diào)用這些對(duì)象上的clone()方法來(lái)創(chuàng)建這些對(duì)象的副本。這意味著克隆對(duì)象和原始對(duì)象將是100%不相交的。它們是100%相互獨(dú)立的。對(duì)克隆對(duì)象所做的任何更改都不會(huì)反映在原始對(duì)象中,反之亦然。
實(shí)現(xiàn)拷貝的方法:
  1. 繼承Cloneable接口并重寫clone()方法
    如果是淺拷貝,只需要讓外層的對(duì)象重寫clone()方法.
    如果要實(shí)現(xiàn)深拷貝, 則需要逐層實(shí)現(xiàn)Cloneable接口實(shí)現(xiàn)clone()方法.
  2. 拷貝構(gòu)造器
    最常用的方法, 通過(guò)構(gòu)造方法或者靜態(tài)工廠方法來(lái)創(chuàng)建原對(duì)象的拷貝.
  3. 通過(guò)序列化的方式
    如果對(duì)象的嵌套層次很深, 或者后續(xù)修改增加了一些字段, 這時(shí)候維護(hù)clone()方法或者手動(dòng)構(gòu)造對(duì)象都很麻煩.
    這時(shí)候可以考慮使用Serializable反序列化來(lái)構(gòu)建一個(gè)新的對(duì)象. 反序列化出的對(duì)象和原對(duì)象內(nèi)存地址是完全獨(dú)立的,屬于深拷貝.

二、不可變類Immutable

上面提到了通過(guò)深拷貝可以創(chuàng)建和原對(duì)象互不影響的拷貝, 但是維護(hù)起來(lái)非常麻煩.
Java提供了另外一種方式來(lái)保證這種獨(dú)立性, 他在被創(chuàng)建后其內(nèi)部狀態(tài)就不能被修改, 也稱作Immutable對(duì)象. JDK中的Immutable對(duì)象包括String、基本類型的包裝類(Integer,Double,Float...)、BigDecimal,BigInteger.
一個(gè)Immutable類想要維持不可變性, 需要遵循以下規(guī)則:

1. 類用final修飾 或者 私有構(gòu)造器

不管是final修飾類還是私有構(gòu)造器, 都是為了防止被繼承.
如果不可變類能被繼承, 由于父類引用指向子類的實(shí)例時(shí), 很明顯我們沒法約束每個(gè)子類的不可變性, 那么父類的不可變性就會(huì)遭到破壞.

2. 類中的屬性聲明為private,并且不對(duì)外界提供setter方法

從訪問級(jí)別上控制不可變性.

3. 類中的屬性聲明為final

如果屬性是基本類型,那么聲明為final后就不能改變.
如果屬性是引用類型, final只是聲明這個(gè)對(duì)象的引用不能改變, 注意對(duì)象的屬性還是可以改變的.所以有第四點(diǎn)來(lái)補(bǔ)充

4. 如果類中存在可變類的屬性, 當(dāng)我們?cè)L問他的時(shí)候需要進(jìn)行保護(hù)性拷貝.

如果類中存在可變類的變量, 雖然我們已經(jīng)對(duì)他加上了final修飾符, 但這僅僅表示這個(gè)變量的引用不能指向別的地址. 但是我們還是可以通過(guò)可變屬性的引用來(lái)修改他可變類內(nèi)部的屬性, 從而破壞可變類對(duì)象調(diào)用者的不可變性.
在構(gòu)造器, 訪問方法, 和序列化的readObject方法中, 如果用到了這個(gè)可變對(duì)象的變量, 我們需要對(duì)他進(jìn)行保護(hù)性拷貝, 避免通過(guò)可變的引用影響到他的調(diào)用者.

三、保護(hù)性拷貝

在構(gòu)造器, getter方法, 序列化的readObject方法(隱式構(gòu)造器)中, 進(jìn)行保護(hù)性拷貝(defensive copies)來(lái)返回對(duì)象的拷貝 而不是 對(duì)象本身.

看EffectiveJava中的例子, Period是一個(gè)描述日期的類, 他的構(gòu)造方法進(jìn)行了參數(shù)合法性檢查start < end

// Broken "immutable" time period class
public final class Period {
    private final Date start;
    private final Date end;

    /**
     * @param  start the beginning of the period
     * @param  end the end of the period; must not precede start
     * @throws IllegalArgumentException if start is after end
     * @throws NullPointerException if start or end is null
     */
    public Period(Date start, Date end) {
        if (start.compareTo(end) > 0)
            throw new IllegalArgumentException(
                start + " after " + end);

        this.start = start;
        this.end   = end;
    }

    public Date start() {
        return start;
    }

    public Date end() {
        return end;
    }
    ...    // Remainder omitted
}
構(gòu)造器

在構(gòu)造方法執(zhí)行后, 由于Date是不可變對(duì)象, 我們可以引用start,end所指向的變量進(jìn)行外界的修改.

// Attack the internals of a Period instance
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78);  // Modifies internals of p!

顯然我們這里不希望在對(duì)象初始化后受到外界的影響來(lái)破壞start < end的約束
進(jìn)行保護(hù)性拷貝后, 直接使用參數(shù)構(gòu)建一個(gè)新的對(duì)象. 這樣外部的修改根本不會(huì)影響到新的對(duì)象.

// Repaired constructor - makes defensive copies of parameters
public Period(Date start, Date end) {
    this.start = new Date(start.getTime());
    this.end   = new Date(end.getTime());

    if (this.start.compareTo(this.end) > 0)
      throw new IllegalArgumentException(
          this.start + " after " + this.end);
}
getter方法

同理, 對(duì)于getter方法暴露宿主類內(nèi)部可變對(duì)象的引用時(shí), 也要進(jìn)行保護(hù)性拷貝防止外部通過(guò)引用來(lái)修改, 影響到宿主類.

// Repaired accessors - make defensive copies of internal fields
public Date start() {
    return new Date(start.getTime());
}

public Date end() {
    return new Date(end.getTime());
}
readObject()

如果對(duì)象實(shí)現(xiàn)了Serializable, 在反序列化readObject方法中, 我們知道序列化反序列化是通過(guò)流的方式進(jìn)行的, 攻擊者可以偽造一個(gè)流來(lái)修改對(duì)象內(nèi)可變參數(shù), 對(duì)于這些可變參數(shù)我們也要進(jìn)行保護(hù)性拷貝.

// readObject method with defensive copying and validity checking
private void readObject(ObjectInputStream s)
            throws IOException, ClassNotFoundException {
    s.defaultReadObject();
    // Defensively copy our mutable components
    start = new Date(start.getTime());
    end = new Date(end.getTime());
    // Check that our invariants are satisfied
    if (start.compareTo(end) > 0)
    throw new InvalidObjectException(start +" after "+ end);
}
注意點(diǎn):
  1. 保護(hù)拷貝不止針對(duì)不可變類, 對(duì)于可變類, 只要他內(nèi)部的可變對(duì)象暴露的引用可能會(huì)影響到他的內(nèi)部穩(wěn)定性, 我們就需要對(duì)他的這個(gè)可變對(duì)象進(jìn)行保護(hù)性拷貝.
  2. 不管是可變類還是不可變類,只要有可能,應(yīng)該盡量使用不可變類型和基本數(shù)據(jù)類型作為類的屬性. 基本類型是不存在拷貝的概念完全獨(dú)立的, 而Immutable對(duì)象在初始化后就不會(huì)發(fā)生改變, 我們不需要也不應(yīng)該對(duì)他做拷貝處理.
  3. 拷貝一個(gè)對(duì)象時(shí), 最好使用構(gòu)造器或者靜態(tài)工廠來(lái)進(jìn)行拷貝, 而不是調(diào)用它的clone()方法(因?yàn)閷?duì)于不可變類可能是沒有final修飾的, 他的子類可能會(huì)重寫clone()方法)
  4. 保護(hù)性拷貝是視情況而定的, 如果你不需要保持對(duì)象內(nèi)部的穩(wěn)定性, 那么不需要對(duì)暴露的可變對(duì)象屬性做處理.

四、不可變類的優(yōu)缺點(diǎn)

優(yōu)點(diǎn)

1. 安全性高. 不可變類的對(duì)象被聲明后就不能改變
不可變對(duì)象作為屬性被別的對(duì)象使用后, 對(duì)于調(diào)用者來(lái)說(shuō)我們無(wú)需擔(dān)心賦值后, 不可變類的后續(xù)修改會(huì)影響到調(diào)用者.
要注意這里是: 不可變類對(duì)象內(nèi)容不能修改,但并不代表其引用不能改變. 舉個(gè)例子

static class StringW{
    private String value;
    public StringW(String value) {
        this.value = value;
    }
}

private static void test2() {
    //修改不可變類String的值
    String strKey = "key";
    String strValue = "value";
    HashMap<String, String> map = new HashMap<>();
    map.put(strKey, strValue);
    HashMap<String, StringW>  maps = new HashMap<>();
    StringW strwValue = new StringW("value");
    maps.put(strKey, strwValue);
   
    //修改不可變類的值
    strValue = "value1111";
    System.out.println(map.get("key"));
    //修改可變類的值
    strwValue.value = "value11111";
    System.out.println(maps.get(strKey).value);
}

打印結(jié)果

value

value11111

可以看到String的修改并沒有影響到他的調(diào)用者
而我們自定義的可變類StringW的修改影響到了他的調(diào)用者.

這是因?yàn)镴ava中我們說(shuō)的對(duì)象分為 對(duì)象開辟的內(nèi)存 和 指向該內(nèi)存地址的引用兩部分.

  • 不可變對(duì)象在聲明賦值后, 后續(xù)的修改并不是在原對(duì)象上進(jìn)行的, 而是直接斷開指向原對(duì)象內(nèi)存的引用, 重新在堆區(qū)新建一個(gè)對(duì)象并指向新的對(duì)象內(nèi)存地址. 而原內(nèi)存的內(nèi)容不會(huì)受到影響.

  • 而可變對(duì)象的修改則是在原對(duì)象上進(jìn)行的, 只要指向可變對(duì)象的引用都會(huì)受到影響.

2. 線程安全
不可變類型的對(duì)象在創(chuàng)建后就不會(huì)被修改,所以我們不需要考慮多線程下對(duì)象的讀寫造成的同步問題. 他是線程安全的.

缺點(diǎn)

由于不可變類創(chuàng)建后就不能改變的特性, 在頻繁改變值的場(chǎng)景下, 不可變類的引用需要不斷的斷開與原來(lái)對(duì)象內(nèi)存的鏈接, 并指向新的對(duì)象內(nèi)存區(qū). 最明顯的就是String類, 我們每修改一次String, 就會(huì)在內(nèi)存創(chuàng)建一個(gè)新的String對(duì)象, 原有的就會(huì)被丟棄.
例如

String string = "a";
string = string + "b";
string = string + "c";
string = "3"+string;

中間的過(guò)程內(nèi)存里會(huì)創(chuàng)建"a","b","ab","c","abc"等大量的對(duì)象, 很明顯我們只關(guān)心最后的結(jié)果, 無(wú)需開辟這么多的內(nèi)存空間, 尤其是在移動(dòng)端上. 我們可以使用StringBuilder來(lái)直接對(duì)一塊內(nèi)存進(jìn)行修改.

Java中也針對(duì)這種情況做了優(yōu)化. String類有位于方法區(qū)的常量池保存這些創(chuàng)建過(guò)的字符串變量, 這個(gè)方法區(qū)被所有的線程共享.
而Byte, Short, Integer, Long, Character, Boolean, Float, Double, 除Float和Double以外, 其它六種都實(shí)現(xiàn)了常量池, 但是它們只在大于等于-128并且小于等于127時(shí)才使用常量池。以Character為例, 調(diào)用valueOf(char c)創(chuàng)建對(duì)象的時(shí)候會(huì)優(yōu)先取靜態(tài)內(nèi)部類CharacterCache緩存的值.

    public static Character valueOf(char c) {
        if (c <= 127) { // must cache
            return CharacterCache.cache[(int)c];
        }
        return new Character(c);
    }

    private static class CharacterCache {
        private CharacterCache(){}

        static final Character cache[] = new Character[127 + 1];

        static {
            for (int i = 0; i < cache.length; i++)
                cache[i] = new Character((char)i);
        }
    }

五、破壞不可變性的方法

通過(guò)反射我們可以繞過(guò)不可變類的限制, 從而修改他內(nèi)部的屬性來(lái)破壞不可變性.

String str = "12345";
//獲取String類中的value字段
Field valueFieldOfString = String.class.getDeclaredField("value");
//改變value屬性的訪問權(quán)限
valueFieldOfString.setAccessible(true);
//獲取s對(duì)象上的value屬性的值
char[] value = (char[]) valueFieldOfString.get(str);
//修改數(shù)組末位
value[4] = '0';  
System.out.println("str = " + str);

輸出結(jié)果

str = 12340

可見通過(guò)反射,可以破壞不可變類的不可變性.

總結(jié)

  • 深拷貝淺拷貝取決于 原對(duì)象和拷貝對(duì)象是否完全獨(dú)立, 都可以通過(guò)覆蓋clone()方法或者手動(dòng)構(gòu)造對(duì)象來(lái)實(shí)現(xiàn).
  • 對(duì)于基本數(shù)據(jù)類型, 不存在拷貝的概念, 他們會(huì)重新開辟一塊內(nèi)存.
  • Immutable類不需要進(jìn)行拷貝操作,他們本身就是不可變的.
    如果Immutable對(duì)象的引用指向了一個(gè)新對(duì)象, 那么他會(huì)斷開和原對(duì)象的引用鏈再指向新的對(duì)象, 這時(shí)指向原對(duì)象的其他引用是不會(huì)受到影響的.
  • 對(duì)于對(duì)象內(nèi)的可變類型參數(shù), 如果對(duì)外暴露了可變類型參數(shù)的引用, 需要視情況進(jìn)行保護(hù)性拷貝來(lái)返回可變類型對(duì)象的拷貝而不是對(duì)象本身.
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容