String 爲何不可變 ?

衆所周知, String 是一個不可變的,由 final 修飾的類。那麼它的不可變性體如今哪裏呢? 看下面一段簡單的代碼:html

String str= "123";
  str = "456";
複製代碼

相信應該沒人會以爲這段代碼是錯誤的,那麼這符合 String 的不可變性嗎?String 的不可變性是如何體現的? 不可變性的好處是什麼?帶着這些疑問,read the fuck source code !java

什麼是不可變類 ?

Effective Java 中第 15 條 使可變性最小化 中對 不可變類 的解釋:數組

不可變類只是其實例不能被修改的類。每一個實例中包含的全部信息都必須在建立該實例的時候就提供,而且在對象的整個生命週期內固定不變。爲了使類不可變,要遵循下面五條規則:緩存

1. 不要提供任何會修改對象狀態的方法。安全

2. 保證類不會被擴展。 通常的作法是讓這個類稱爲 final 的,防止子類化,破壞該類的不可變行爲。bash

3. 使全部的域都是 final 的。微信

4. 使全部的域都成爲私有的。 防止客戶端得到訪問被域引用的可變對象的權限,並防止客戶端直接修改這些對象。函數

5. 確保對於任何可變性組件的互斥訪問。 若是類具備指向可變對象的域,則必須確保該類的客戶端沒法得到指向這些對象的引用。ui

在 Java 平臺類庫中,包含許多不可變類,例如 String , 基本類型的包裝類,BigInteger, BigDecimal 等等。綜上所述,不可變類具備一些顯著的通用特徵:類自己是 final 修飾的;全部的域幾乎都是私有 final 的;不會對外暴露能夠修改對象屬性的方法。經過查閱 String 的源碼,能夠清晰的看到這些特徵。this

爲何 String 不可變 ?

String 的源碼實現

回到開頭提出的問題,對於這段代碼:

String str= "123";
str = "456";
複製代碼

下面這張圖清楚地解釋了代碼的執行過程:

執行第一行代碼時,在堆上新建一個對象實例 123 ,str 是一個指向該實例的引用,引用包含的僅僅只是實例在堆上的內存地址而已。執行第二行代碼時,僅僅只是改變了 str 這個引用的地址,指向了另外一個實例 456。因此,正如前面所說過的,不可變類只是其實例不能被修改的類。str 從新賦值僅僅只是改變了它的引用而已,並不會真正去改變它原本的內存地址上的值。這樣的好處也是顯而易見的,最簡單的當存在多個 String 的引用指向同一個內存地址時,改變其中一個引用的值並不會對其餘引用的值形成影響。固然還有其餘的一些好處,這個放在後面再總結。

那麼,String 是如何保持不可變性的呢?結合 Effective Java 中總結的五條原則,閱讀它的 源碼 以後就一清二楚了。

看一下 String 的幾個域:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

    /** Cache the hash code for the string */
    private int hash; // Default to 0

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    private static final long serialVersionUID = -6849794470754667710L;

    /**
     * Class String is special cased within the Serialization Stream Protocol.
     *
     * A String instance is written into an ObjectOutputStream according to
     * <a href="{@docRoot}/../platform/serialization/spec/output.html">
     * Object Serialization Specification, Section 6.2, "Stream Elements"</a>
     */
    private static final ObjectStreamField[] serialPersistentFields =
        new ObjectStreamField[0];

        ...
        ...
}
複製代碼

String 類是 final 修飾的,知足第二條原則:保證類不會被擴展。 分析一下它的幾個域:

  • private final char value[] : 能夠看到 Java 仍是使用字節數組來實現字符串的,而且用 final 修飾,保證其不可變性。這就是爲何 String 實例不可變的緣由。

  • private int hash : String的哈希值緩存

  • private static final long serialVersionUID = -6849794470754667710L : String對象的 serialVersionUID

  • private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0] : 序列化時使用

其中最主要的域就是 value,表明了 String對象的值。因爲使用了 private final 修飾,正常狀況下外界沒有辦法去修改它的值的。正如第三條 使全部的域都是 final 的。 和第四條 使全部的域都成爲私有的 所描述的。難道這樣一個 private 加上 final 就能夠保證萬無一失了嗎?看下面代碼示例:

final char[] value = {'a', 'b', 'c'};
    value[2] = 'd';
複製代碼

這時候的 value 對象在內存中已是 a b d 了。其實 final 修飾的僅僅只是 value 這個引用,你沒法再將 value 指向其餘內存地址,例以下面這段代碼就是沒法經過編譯的:

final char[] value = {'a', 'b', 'c'};
    value = {'a', 'b', 'c', 'd'};
複製代碼

因此僅僅經過一個 final 是沒法保證其值不變的,若是類自己提供方法修改實例值,那就沒有辦法保證不變性了。Effective Java 中的第一條原則 不要提供任何會修改對象狀態的方法 。String 類也很好的作到了這一點。在 String 中有許多對字符串進行操做的函數,例如 substring concat replace replaceAll 等等,這些函數是否會修改類中的 value 域呢?咱們看一下 concat() 函數的內部實現:

public String concat(String str) {
    int otherLen = str.length();
    if (otherLen == 0) {
        return this;
    }
    int len = value.length;
    char buf[] = Arrays.copyOf(value, len + otherLen);
    str.getChars(buf, len);
    return new String(buf, true);
}
複製代碼

注意其中的每一步實現都不會對 value產生任何影響。首先使用 Arrays.copyOf() 方法來得到 value 的拷貝,最後從新 new 一個String對象做爲返回值。其餘的方法和 contact 同樣,都採起相似的方法來保證不會對 value 形成變化。的的確確,String 類中並無提供任何能夠改變其值的方法。相比 final 而言,這更能保障 String 不可變。

String 對象在內存中的位置

關於 Java 內存區域的相關知識能夠閱讀 《深刻理解 Java 虛擬機》 一書中相關章節或者個人對應的 讀書筆記

細心的讀者應該發現上面出現過兩種 String 對象的寫法:

String str1 = "123";
  String str2 = new String("123");
  System.out.println(str1 == str2);
複製代碼

結果顯然是 false。咱們都知道字符串常量池的概念,JVM 爲了字符串的複用,減小字符串對象的重複建立,特別維護了一個字符串常量池。第一種字面量形式的寫法,會直接在字符串常量池中查找是否存在值 123,若存在直接返回這個值的引用,若不存在建立一個值爲 123 的 String 對象並存入字符串常量池中。而使用 new 關鍵字,則會直接在堆上生成一個新的 String 對象,並不會理會常量池中是否有這個值。因此本質上 str1str2 指向的內存地址是不同的。

那麼,使用 new 關鍵字生成的 String 對象能夠進入字符串常量池嗎?答案是確定的,String 類提供了一個 native 方法 intern() 用來將這個對象加入字符串常量池:

String str1 = "123";
  String str2 = new String("123");
  str2=str2.intern();
  System.out.println(str1 == str2);
複製代碼

打印結果爲 truestr2 調用 intern() 函數後,首先在字符串常量池中尋找是否存在值爲 123 的對象,若存在直接返回該對象的引用,若不存在,加入 str2 並返回。上述代碼中,常量池中已經存在值爲 123str1 對象,則直接返回 str1 的引用地址,使得 str1str2 指向同一個內存地址。

那麼說了半天的字符串常量池在內存中處於什麼位置呢?常量池是方法區的一部分,用於存放編譯期生成的各類字面量和符號引用,運行期間也有可能將新的常量放入池中。在 Java 虛擬機規範中把方法區描述爲堆的一個邏輯部分,但它卻有一個別名叫 Non-Heap,目的應該是爲了和 Java 堆區分開。

不可變類的好處

Effective Java 中總結了不可變類的特色。

  • 不可變類比較簡單。
  • 不可變對象本質上是線程安全的,它們不要求同步。不可變對象能夠被自由地共享。
  • 不只能夠共享不可變對象,甚至能夠共享它們的內部信息。
  • 不可變對象爲其餘對象提供了大量的構建。
  • 不可變類真正惟一的缺點是,對於每一個不一樣的值都須要一個單獨的對象。

String 真的不可變嗎?

綜上所述,String 的確是個不可變類,可是真的沒有辦法改變 String 對象的值嗎?答案確定是否認的,反射機制能夠作到不少日常作不到的事情。

String str = "123";
System.out.println(str);
Field field = String.class.getDeclaredField("value");
field.setAccessible(true);
char[] value = (char[]) field.get(str);
value[1] = '3';
複製代碼

執行結果:

123
  133
複製代碼

很顯然,經過反射能夠破壞 String 的不可變性。

總結

除了 String 類,系統類庫中還提供了一些其餘的不可變類,基本類型的包裝類、BigInteger、BigDecimal等等。這些不可變類比可變類更加易於設計、實現和使用,不容易出錯且更加安全。另外,要記住並不只僅是靠一個 final 關鍵字來實現不可變的,更多的是靠類內部的具體實現細節。

文章同步更新於微信公衆號: 秉心說 , 專一 Java 、 Android 原創知識分享,LeetCode 題解,歡迎關注!

相關文章
相關標籤/搜索