為什么String類是不可變的?


為什么String類是不可變的?
#

String類

什么是不可變對象

  當滿足以下條件時,對象才是不可變的:

  這是《Java並發編程實戰》一書中的定義。在書中,說明並不是一定要將所有的域都設為final類型,比如String類就是這種情況,String會將散列值的計算推遲到第一次調用hashCode()時進行,並將計算得到的散列值緩存到非final類型的域中,但這種方式之所以可行,是因為在每次計算時都得到相同的結果,這基於一個不可變的狀態(書中指出:自己編寫代碼時不要這么做,推薦全都設為final)。
  因此,不可變對象可以理解為:如果一個對象,在它正確創建完成之后,不能再改變它的狀態(包括基本數據類型的值不能改變,引用類型的變量不能指向其他的對象,引用類型指向的對象的狀態也不能改變),那么這個對象就是不可變的。

“不可變的對象”與“不可變的對象引用”區別

比如:

String str = "test";
str = "test1";

我們從下圖可以看到,當定義String str = "test1"時,其實不是真正改變了str的內容,而是改變了str的引用。

  那么何為"不可變的對象引用"呢?final只保證引用類型變量所引用的地址不會改變,即一直引用同一個對象,但是這個對象的內容(對象的非final成員變量的值可以改變)完全可以發生改變(比如final int[] intArray;,intArray不允許再引用其他對象,但是intArray內的int值卻可以被修改)。

為什么String對象是不可變的?

  要理解String的不可變性,首先看一下String類中都有哪些成員變量。 在JDK1.8中,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

  其中,成員變量hash並沒有用final聲明,但是由於第一次調用hashCode()會重新計算hash值,並且以后調用會使用已緩存的值,當然最關鍵的是每次計算時都得到相同的結果,所以也保證了對象的不可變。

    public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }

  在Java中,數組也是對象, 所以value也只是一個引用,它指向一個真正的數組對象。其實執行了String s = “ABCabc”; 這句代碼之后,真正的內存布局應該是這樣的:

  value是String封裝的數組,value中的所有字符都是屬於String這個對象的。由於value是private的,並且沒有提供setValue等公共方法來修改這些值,所以在String類的外部無法修改String。也就是說一旦初始化就不能修改。此外,value變量是final的, 也就是說在String類內部,一旦這個值初始化了,value引用類型變量所引用的地址不會改變,即一直引用同一個對象。所以可以說String對象是不可變對象。但其實value所引用對象的內容完全可以發生改變(反射消除String類對象的不可變特性)。

如何理解substring, replace, replaceAll, toLowerCase等方法

比如:

String a = "ABCabc";  
System.out.println("a = " + a);  
a = a.replace('A', 'a');  
System.out.println("a = " + a);  

  a的值看似改變了,其實也是同樣的誤區。再次說明, a只是一個引用, 不是真正的字符串對象,在調用a.replace('A', 'a')時, 方法內部創建了一個新的String對象,並把這個新的對象重新賦給了引用a。String中replace方法的源碼可以說明問題:

    public String replace(char oldChar, char newChar) {
        if (oldChar != newChar) {
            int len = value.length;
            int i = -1;
            char[] val = value; /* avoid getfield opcode */

            while (++i < len) {
                if (val[i] == oldChar) {
                    break;
                }
            }
            if (i < len) {
                char buf[] = new char[len];
                for (int j = 0; j < i; j++) {
                    buf[j] = val[j];
                }
                while (i < len) {
                    char c = val[i];
                    buf[i] = (c == oldChar) ? newChar : c;
                    i++;
                }
                return new String(buf, true);
            }
        }
        return this;
    }

String類不可變性的好處

  1. 只有當字符串是不可變的,字符串池才有可能實現。字符串池的實現可以在運行時節約很多heap空間,因為不同的字符串引用可以指向池中的同一個字符串。但如果字符串是可變的,如果變量改變了它的值,那么其它指向這個值的變量的值也會一起改變。
  2. 如果字符串是可變的,那么會引起很嚴重的安全問題。譬如,數據庫的用戶名、密碼都是以字符串的形式傳入數據庫,以獲得數據庫的連接,或者在socket編程中,主機名和端口都是以字符串的形式傳入。因為字符串是不可變的,所以它的值是不可改變的,否則黑客們可以鑽到空子,改變字符串指向的對象的值,造成安全漏洞。
  3. 因為字符串是不可變的,所以是多線程安全的,同一個字符串實例可以被多個線程共享。這樣便不用因為線程安全問題而使用同步。
  4. 類加載器要用到字符串,不可變性提供了安全性,以便正確的類被加載。譬如你想加載java.sql.Connection類,而這個值被改成了myhacked.Connection,那么會對你的數據庫造成不可知的破壞。
  5. 因為字符串是不可變的,所以在它創建的時候hashcode就被緩存了,不需要重新計算,這就使得字符串很適合作為Map中的鍵,字符串的處理速度要快過其它的鍵對象。這就是HashMap中的鍵往往都使用字符串的原因。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM