最近,又有好幾個小夥伴問我這個問題:“二哥,爲什麼 Java 的 String 要設計成不可變的啊?”說實話,這也是一道非常經典的面試題,面試官超喜歡問。我之前寫過這方面的文章,現在讀起來似乎不太滿意,所以我決定再囉嗦最後一次,交出一份更滿意的答卷,讓小夥伴們在面試官面前更從容一些,更有底氣一些。

關於不可變對象,還有這樣一個小故事。Java 之父詹姆斯高司令曾在一次採訪中被問及這樣一個問題:“高司令,應該什麼時候使用不可變對象啊?”你猜高司令怎麼回答?

如有可能,我願意任何時候都使用不可變對象。

這就是高司令的答案,那有的小夥伴可能不服,老人家會說中文,你瞎扯吧你。也對哈,那就上英文唄:

I would use an immutable whenever I can.

這下徹底被打服了吧?老人家還說,不可變有着非常強大的功能,比如說,緩存、安全性、高性能等等。

01、什麼是不可變對象

不可變對象在創建後,它的內部狀態會保持不變,這就意味着,一旦我們將一個對象分配給一個變量,就無法再通過任何方式更改對象的狀態了。

關於不可變對象的更多信息,可以查看我之前寫的另外一篇文章—— 這次要說不明白immutable類,我就怎麼地 ,看完啥都明白了。你看,寫系列文章的好處就是這樣,不需要重複造輪子,用到的時候直接搬出來套上就行了。

02、爲什麼 String 是不可變的

重點來了啊,爲什麼 String 是不可變的?原因可以從四個方面說起,緩存、安全性、同步和高性能。

1)字符串常量池

字符串恐怕是 Java 中最常用的數據形式了,如果字符串非要謙虛地說自己是老二,就沒有人敢說自己是老大。

因此,把字符串緩存起來,並且重複使用它們會節省大量堆空間(堆內存用來存儲 Java 中的對象,無論是成員變量、局部變量,還是類變量,它們指向的對象都存儲在堆內存中),因爲不同的字符串變量引用的是字符串常量池中的同一個對象。這也正是字符串常量池存在的目的。

字符串常量池是 Java 虛擬機用來存儲字符串的一個特殊的區域,由於字符串是不可變的,因此 Java 虛擬機可以在字符串常量池中只爲同一個字符串存儲一個字符串副本來節省空間。

字符串常量池的主要使用方法有兩種:

  • 直接使用雙引號聲明出來的字符串對象會直接存儲在常量池中。

  • 否則,可以使用 String 類提供的 intern() 方法強制將當前字符串放入常量池中——常量池中查詢不到當前字符串。

來看下面這段代碼:

String s1 = "沉默王二";
String s2 = "沉默王二";

System.out.println(s1 == s2); // true

由於字符串常量池的存在,所以兩個不同的變量都指向了池中同一個字符串對象,從而節省了稀缺的內存資源。如果是通過 new 關鍵字創建的對象,則需要新的堆空間。

放心,關於字符串常量池,後面有時間的話,我再單獨寫一篇文章詳細地說一說。

2)安全性

字符串在 Java 應用程序中的使用範圍非常廣,幾乎無處不在,比如說存儲用戶名、密碼、數據庫連接地址等等這些非常敏感的信息,因此,必須要保證 String 類的絕對安全性。

來考慮一下下面這段代碼:

void criticalMethod(String userName) {
    // 檢查用戶名是否合法
    if (!isAlphaNumeric(userName)) {
        throw new SecurityException(); 
    }

    // 初始化數據庫連接
    initializeDatabase();

    // 準備修改用戶狀態
    connection.executeUpdate("UPDATE members SET status = 'active' " +
      " WHERE username = '" + userName + "'");
}

通常情況下,用戶名由客戶端傳遞到服務器端,服務器端接收後要先對用戶名進行檢查,再進行其他操作,因爲客戶端傳遞過來的信息不一定值得信任。

如果字符串是可變的,那麼我們在執行 executeUpdate 更新數據庫的時候,就有點不放心,因爲即便是安全性檢查通過了,字符串仍然有可能被修改。

在調用 isAlphaNumeric() 方法進行安全性檢查期間,userName 的值仍然有可能被 criticalMethod() 方法的調用者進行篡改,就容易造成 SQL 注入。

但如果字符串是不可變的,這方面的擔憂就不存在了。因爲在執行更新之前,字符串的值是確定的,就是我們檢查安全性之後的值。

3)線程安全

由於字符串是不可變的,因此可以在多線程之間共享,如果一個線程把字符串的值修改爲另外一個,那麼就會在字符串常量池中創建另外一個字符串,原有的字符串仍然會保持不變。

不過,很遺憾,我還不知道怎麼從代碼層面上去證明這一點,只能純理論 yy 一下。小夥伴誰有辦法的,教教我,在線等的那種。

4)哈希碼

字符串廣泛應用於 HashMap、HashTable、HashSet 等需要哈希碼作爲鍵的數據結構中,在對這些哈希表進行操作的時候,需要頻繁調用 hashCode() 方法來獲取鍵的哈希碼。

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

由於字符串是不可變性,這就保證了鍵值的哈希值不會發生改變,因此在第一次調用 String 類的 hashCode() 方法時,就對哈希值進行了緩存,此後,就一直返回相同的值。

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

public int hashCode() {
    int h = hash;
    if (h == 0 && !hashIsZero) {
        h = isLatin1() ? StringLatin1.hashCode(value)
                : StringUTF16.hashCode(value);
        if (h == 0) {
            hashIsZero = true;
        } else {
            hash = h;
        }
    }
    return h;
}

由於哈希值被緩存了,這在另外一種層面上提高了哈希表的訪問性能,因爲哈希值不用重新計算了。

假如字符串是可變的,那就意味着哈希碼會有多個,在通過鍵獲取值的時候,就不一定能夠獲取到對的值了。

你看,字符串常量池的存在,哈希碼的存在,在很大程度上提高了程序的性能。

03、總結

好了,我親愛的小夥伴們,以上就是本文的全部內容了。我相信你一定對字符串的不可變性有了充足的瞭解,由於字符串是不可變的,因此我們可以將它看作是一個特殊的基本數據類型,哪怕是在多線程的環境下,也不用擔心它的值是否會發生改變。

------------------

公衆號:沉默王二(ID:cmower)
CSDN:沉默王二
這是一個有顏值卻靠才華喫飯的程序員,你知道,他的文章風趣幽默,讀起來就好像花錢一樣爽快。

長按下圖二維碼關注,你將感受到一個有趣的靈魂, 且每篇文章都有乾貨。

-------------- --- -

原創不易,莫要白票,覺得有點用的話,就請你爲本文點個 在看 ,或者無情地 轉發 ,因爲這將是我寫作更多優質文章的最強動力。

相關文章