1. 程式人生 > >靈魂拷問:為什麼 Java 字串是不可變的?

靈魂拷問:為什麼 Java 字串是不可變的?

在逛 programcreek 的時候,發現了一些精妙絕倫的主題。比如說:為什麼 Java 字串是不可變的?像這類靈魂拷問的主題,非常值得深思。

對於絕大多數的初級程式設計師來說,往往停留在“知其然不知其所以然”的層面上——會用,但要說底層的原理,可就只能撓撓頭雙手一攤一張問號臉了。

很長一段時間內,我也一直處於這種層面上。導致的局面就是,我在挖一些高深點的技術方案時,往往束手無策;在讀一些高深點的技術文章時,往往理解不了作者在說什麼。

藉此機會,我就和大家一起,對“為什麼 Java 字串是不可變的”進行一次深入地研究。注意了,準備打怪升級了!

01、圖文分析

來看下面這行程式碼。

String alita = "阿麗塔";

這行程式碼在字串常量池中建立了一個內容為“阿麗塔”的物件,並將其賦值給了字串變數 alita(儲存的是字串物件"阿麗塔"的引用)。如下圖所示。

再來看下面這行程式碼。

String wanger = alita;

這行程式碼將字串變數 alita 賦值給了字串變數 wanger。這時候,wanger 和 alita 儲存的是同一個字串物件的引用。如下圖所示。

再來看下面這行程式碼。

alita = "戰鬥天使".concat(alita);

這行程式碼將字串“戰鬥天使”拼接在字串變數 alita 的前面,並重新賦值給 alita。這個過程就比之前的複雜了。我們需要先來看看 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);
}

可以看得出,"戰鬥天使".concat(alita)

這行程式碼會先在字串常量池中建立一個新的字串物件,內容為“戰鬥天使”,然後 concat() 方法會將其對應的字元陣列和“阿麗塔”對應的字元陣列複製到一個新的字元陣列 buf 中,最後,再通過 new 關鍵字建立了一個新的字串物件,並返回。如下圖所示。

從上圖中可以得出結論,alita 此時引用的是在堆中新建立的字串物件。

02、物件和物件引用

可能有些讀者看完上面的圖文分析沒有理解反而更疑惑了:alita 不是變了嗎?從“阿麗塔”變為“戰鬥天使阿麗塔”?怎麼還說字串是不可變的呢?

這裡需要給大家解釋一下,什麼是物件,什麼是物件引用。

在 Java 中,由於不能直接操作物件本身,所以就有了物件引用這個概念,物件引用儲存的是物件在記憶體中的地址。

PS:Java 虛擬機器在執行程式的過程中會把記憶體區域劃分為若干個不同的資料區域,如下圖所示。

物件儲存在堆(heap)中,而物件的引用儲存在棧(stack)中。

我們通常所說的“字串是不可變的”是指“字串物件是不可變的”。alita 是字串物件“阿麗塔”或者“戰鬥天使阿麗塔”的引用。這下應該明白了吧?

03、原始碼分析

我們來看一下 String 類的部分原始碼。

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

可以看得出, String 類其實是通過操作字元陣列 value 實現的。而 value 是 private 的,也沒有提供 serValue() 這樣的方法進行修改;況且 value 還是 final 的,意味著 value 一旦被初始化,就無法進行改變。

另外呢,String 類提供的方法,比如說 substring()

public String substring(int beginIndex) {
    int subLen = value.length - beginIndex;
    return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}

toLowerCase()

public String toLowerCase(Locale locale) {
    return new String(result, 0, len + resultOffset);
}

還有之前提到的 concat(),看似都能改變字串的內容,但其實都是在方法內部使用 new 關鍵字重新建立的新字串物件。

04、為什麼要不可變

String 類的原始碼中還有一個重要的欄位 hash,用來儲存字串物件的 hashCode。

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {

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

    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;
    }
}

因為字串是不可變的,所以一旦被建立,它的 hash 值就不會再改變了。由此字串非常適合作為 HashMap 的 key 值,這樣可以極大地提高效率。

另外呢,不可變物件天生是執行緒安全的,因此字串可以在多個執行緒之間共享。

舉個反面的例子,假如字串是可變的,那麼資料庫的使用者名稱和密碼(字串形式獲得資料庫連線)將不再安全,一些高手可以隨意篡改,從而導致嚴重的安全問題。

05、最後

總結一下,字串一旦在記憶體中被建立,就無法被更改。String 類的所有方法都不會改變字串本身,而是返回一個新的字串物件。如果需要一個可修改的字元序列,建議使用 StringBuffer 或 StringBuilder 類代替 String 類,否則每次建立的字新符串物件會導致 Java 虛擬機器花費大量的時間進行垃圾回收。


好了各位讀者朋友們,以上就是本文的全部內容了。能看到這裡的都是人才,二哥必須要為你點個贊