天天看點

String是如何保證不可變的

作者:程式員的成長

問題:

  1. String是如何保證不可變的?
  2. String為什麼要設計為不可變的?
  3. StringBuffer為什麼可變?底層邏輯是什麼?
  4. StringBuffer是如何進行擴容的?

String是如何保證不可變的?

String類被final修飾,不能被繼承。底層采用一個 private final char value[] 數組進行存儲,并且使用final修飾,是以String是不可變的。

  1. final修飾的不能被繼承;
  2. final修飾的變量指派後不能改變;
  3. final修飾的方法不能重寫;
String是如何保證不可變的

但是這裡要明确一點,字元數組被 final 修飾,不能改變的是變量的引用位址,并不是字元數組的中的元素。可以對字元數組中的元素進行改變。

public static void main(String[] args) throws Exception {
    final char[] chars = new char[]{'A', 'B', 'C'};
    chars[0] = 'D';
    System.out.println(chars);
}           

既然字元數組是可變的,那為什麼String又是不可變的呢?其實更多的是在底層實作的。通過源碼可以發現 char 數組是 private 修飾的,并且沒有對外部提供任何修改 char 數組的方法。String被 final 修飾不可被繼承,也無法通過繼承重寫方法的方式進行破壞。通過這三種手段保證了 String 的不可變性。

String 為什麼設計為不可變的?

更容易實作字元串池

字元串池是用來存儲Java常量的一塊空間。主要用來緩存和重用字元串對象,進而提高性能和節省記憶體。當我們聲明一個字元串常量時,會先檢查字元串池是否存在相同内容的字元串常量。如果存在,則直接傳回字元串池中的對象引用;如果不存在,則在字元串池中建立一個新的字元串對象,并傳回對象的引用。

保證多線程安全

并發場景下,多個線程讀取同一個資源并不會引發線程問題的。但是多個線程對同一資源進行寫操作是不安全的,是以 String 通過它的不可變性,進而保證了多線程的安全問題。

避免安全問題

在網絡連接配接和資料庫連接配接中經常使用字元串作為參數,例如,網絡連接配接位址URL,反射機制所需要的 String 參數,其不變性可以保證連接配接的安全性。如果字元串是可變的,那麼可以通過修改字元串指向對象的值進行破壞,可能會引起嚴重的安全問題。

加快字元處理速度

因為 String 不可變,保證了 hashcode 的唯一性,那麼在建立對象時就可以将 hashcode 緩存,不需要重新參與計算。這也是 Map 喜歡使用 String 作為 Key 的原因,處理速度比其他的鍵對象要快。是以 HashMap 經常選用 String 作為鍵。

StringBuilder:

非線程安全,StringBuilder底層采用一個可變的 char[] 數組。也就是說StringBuilder底層這個 char 數組是可以進行擴容的。

改變字元串的底層邏輯:

StringBuilder 使用了 append() 進行了字元串的拼接,底層是使用了數組拷貝的方式進行資料的指派,将要拼接的字元數組 copy 到原字元數組中去,并且會在 copy 之前校驗原數組是否需要擴容。

public AbstractStringBuilder append(String str) {
    if (str == null)
        return appendNull();
    int len = str.length();
  // 檢驗是否需要擴容
    ensureCapacityInternal(count + len);
  // 數組copy
    str.getChars(0, len, value, count);
    count += len;
    return this;
}

public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
    if (srcBegin < 0) {
        throw new StringIndexOutOfBoundsException(srcBegin);
    }
    if (srcEnd > value.length) {
        throw new StringIndexOutOfBoundsException(srcEnd);
    }
    if (srcBegin > srcEnd) {
        throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
    }
    System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
}           

StringBuilder擴容:

StringBuilder在初始化的時候,預設是char數組長度是16,每次擴容是原char數組長度的是 2n+2。

StringBuffer:

線程安全,相比 StringBuilder 來說在原來的方法上使用了 synchronized 來保證線程安全。

性能比較:

StringBuilder > StringBuffer > String

總結:

如果遇到大量的字元串拼接的時候,優先選擇使用StringBuilder或StringBuffer。 String是字元串常量,不可變,使用String進行字元串操作的時候,每次都會建立一個新的對象,原來的對象就會變成垃圾被GC回收掉,很影響效率。 StringBuilder和StringBuffer它們是字元串變量,是可變的。當對字元串進行操作時,實際上實在對象上操作,并不會像String一樣每次建立新的對象。

繼續閱讀