最近,又有好幾個小伙伴問我這個問題:“二哥,為什么 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、總結
好了,我親愛的小伙伴們,以上就是本文的全部內容了,我相信你一定對字串的不可變性有了充足的了解,由于字串是不可變的,因此我們可以將它看作是一個特殊的基本資料型別,哪怕是在多執行緒的環境下,也不用擔心它的值是否會發生改變,
如果覺得文章對你有點幫助,請微信搜索「 沉默王二 」第一時間閱讀,
本文已收錄 GitHub,傳送門~ ,里面更有大廠面試完整考點,歡迎 Star,
我是沉默王二,一枚有顏值卻靠才華茍且的程式員,關注即可提升學習效率,別忘了三連啊,點贊、收藏、留言,我不挑,嘻嘻,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/162001.html
標籤:Java
