天天看點

JAVA的不可變類(immutable)機制與String的不可變性

引用:

https://www.cnblogs.com/jaylon/p/5721571.html

一、不可變類簡介

不可變類:所謂的不可變類是指這個類的執行個體一旦建立完成後,就不能改變其成員變量值。如JDK内部自帶的很多不可變類:Interger、Long和String等。

可變類:相對于不可變類,可變類建立執行個體後可以改變其成員變量值,開發中建立的大部分類都屬于可變類。

不可變類是執行個體建立後就不可以改變成員變量的值。這種特性使得不可變類提供了線程安全的特性但同時也帶來了對象建立的開銷,每更改一個屬性都是重新建立一個新的對象。JDK内部也提供了很多不可變類如Integer、Double、String等。String的不可變特性主要為了滿足常量池、線程安全、類加載的需求。合理使用不可變類可以帶來極大的好處。

二、不可變類的優點

  1. 線程安全

    不可變對象是線程安全的,線上程之間可以互相共享,不需要利用特殊機制來保證同步問題,因為對象的值無法改變。可以降低并發錯誤的可能性,因為不需要用一些鎖機制等保證記憶體的一緻性問題也減少了同步開銷。

  2. 易于構造、使用和測試
  3. ...

三、不可變類的設計方法

設計不可變類的原則:

1. 類添加final修飾符,保證類不被繼承。

如果類可以被繼承會破壞類的不可變性,隻要繼承類覆寫父類的方法并且繼承類可以改變成員變量值,那麼一旦子類以父類的形式出現時,不能保證目前類是不可變的。

2. 保證所有成員變量必須私有,并且加上final修飾

通過這種方式保證成員變量不可改變。但隻做到這一步還不夠,因為如果是對象成員變量有可能在外部改變其值。是以第4點彌補這個不足(深拷貝)。

3. 不提供改變成員變量的方法,包括setter

避免通過其他接口改變成員變量的值,破壞不可變特性。

4.通過構造器初始化所有成員,進行深拷貝(deep copy)

如果構造器傳入的對象直接指派給成員變量,還是可以通過對傳入對象的修改進而導緻改變内部變量的值。例如:

public final class ImmutableDemo {  

    private final int[] myArray;  

    public ImmutableDemo(int[] array) {  

        this.myArray = array; // wrong  

    }  

}

這種方式不能保證不可變性,myArray和array指向同一塊記憶體位址,使用者可以在ImmutableDemo之外通過修改array對象的值來改變myArray内部的值。

為了保證内部的值不被修改,可以采用深度copy來建立一個新記憶體儲存傳入的值。正确做法:

public final class MyImmutableDemo {  

    private final int[] myArray;  

    public MyImmutableDemo(int[] array) {  

        this.myArray = array.clone();   

    }   

}

5. 在getter方法中,不要直接傳回對象本身,而是克隆對象,并傳回對象的拷貝

這種做法也是防止對象外洩,防止通過getter獲得内部可變成員對象後對成員變量直接操作,導緻成員變量發生改變。

四、String對象不可變性的優缺點

從上一節分析,String屬于不可變類,那設定這樣的特性有什麼好處呢?我總結為以下幾點:

1.字元串常量池的需要.

字元串常量池可以将一些字元串常量放在常量池中重複使用,避免每次都重新建立相同的對象、節省存儲空間。但如果字元串是可變的,此時相同内容的String還指向常量池的同一個記憶體空間,當某個變量改變了該記憶體的值時,其他變量的值也會發生改變。是以不符合常量池設計的初衷。

2. 線程安全考慮。

同一個字元串執行個體可以被多個線程共享。這樣便不用因為線程安全問題而使用同步。字元串自己便是線程安全的。

3. 類加載器要用到字元串,不可變性提供了安全性,以便正确的類被加載。

譬如你想加載java.sql.Connection類,而這個值被改成了myhacked.Connection,那麼會對你的資料庫造成不可知的破壞。

4. 支援hash映射和緩存。

因為字元串是不可變的,是以在它建立的時候hashcode就被緩存了(hashCode不可變),不需要重新計算。這使得字元串很适合作為Map中的鍵,字元串的處理速度要快過其它的鍵對象。這就是HashMap中的鍵往往都使用字元串的原因。

缺點:

  1. 如果有對String對象值改變的需求,那麼會建立大量的String對象。

五、String對象的是否真的不可變

雖然String對象将value設定為final,并且還通過各種機制保證其成員變量不可改變。但是還是可以通過反射機制的手段改變其值

//建立字元串"Hello World", 并賦給引用s

    String s = "Hello World"; 

    System.out.println("s = " + s); //Hello World

    //擷取String類中的value字段

    Field valueFieldOfString = String.class.getDeclaredField("value");

    //改變value屬性的通路權限

    valueFieldOfString.setAccessible(true);

    //擷取s對象上的value屬性的值

    char[] value = (char[]) valueFieldOfString.get(s);

    //改變value所引用的數組中的第5個字元

    value[5] = '_';

    System.out.println("s = " + s);  //Hello_World