再見肮雨,Java字符串是不可變的

最近,又有好幾個小伙伴問我這個問題:“二哥晚伙,為什么 Java 的 String 要設(shè)計成不可變的啊俭茧?”說實(shí)話咆疗,這也是一道非常經(jīng)典的面試題,面試官超喜歡問母债。我之前寫過這方面的文章午磁,現(xiàn)在讀起來似乎不太滿意,所以我決定再啰嗦最后一次毡们,交出一份更滿意的答卷迅皇,讓小伙伴們在面試官面前更從容一些,更有底氣一些衙熔。

關(guān)于不可變對象登颓,還有這樣一個小故事。Java 之父詹姆斯高司令曾在一次采訪中被問及這樣一個問題:“高司令红氯,應(yīng)該什么時候使用不可變對象翱蛄?”你猜高司令怎么回答痢甘?

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

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

I would use an immutable whenever I can.

這下徹底被打服了吧悔据?老人家還說,不可變有著非常強(qiáng)大的功能俗壹,比如說科汗,緩存、安全性绷雏、高性能等等头滔。

01、什么是不可變對象

不可變對象在創(chuàng)建后涎显,它的內(nèi)部狀態(tài)會保持不變坤检,這就意味著,一旦我們將一個對象分配給一個變量期吓,就無法再通過任何方式更改對象的狀態(tài)了早歇。

關(guān)于不可變對象的更多信息,可以查看我之前寫的另外一篇文章——這次要說不明白immutable類讨勤,我就怎么地箭跳,看完啥都明白了。你看潭千,寫系列文章的好處就是這樣谱姓,不需要重復(fù)造輪子,用到的時候直接搬出來套上就行了刨晴。

02屉来、為什么 String 是不可變的

重點(diǎn)來了啊,為什么 String 是不可變的狈癞?原因可以從四個方面說起茄靠,緩存、安全性亿驾、同步和高性能嘹黔。

1)字符串常量池

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

因此郭蕉,把字符串緩存起來疼邀,并且重復(fù)使用它們會節(jié)省大量堆空間(堆內(nèi)存用來存儲 Java 中的對象,無論是成員變量召锈、局部變量旁振,還是類變量,它們指向的對象都存儲在堆內(nèi)存中),因?yàn)椴煌淖址兞恳玫氖亲址A砍刂械耐粋€對象拐袜。這也正是字符串常量池存在的目的吉嚣。

字符串常量池是 Java 虛擬機(jī)用來存儲字符串的一個特殊的區(qū)域,由于字符串是不可變的蹬铺,因此 Java 虛擬機(jī)可以在字符串常量池中只為同一個字符串存儲一個字符串副本來節(jié)省空間尝哆。

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

  • 直接使用雙引號聲明出來的字符串對象會直接存儲在常量池中。
  • 否則甜攀,可以使用 String 類提供的 intern() 方法強(qiáng)制將當(dāng)前字符串放入常量池中——常量池中查詢不到當(dāng)前字符串秋泄。

來看下面這段代碼:

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

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

由于字符串常量池的存在,所以兩個不同的變量都指向了池中同一個字符串對象规阀,從而節(jié)省了稀缺的內(nèi)存資源恒序。如果是通過 new 關(guān)鍵字創(chuàng)建的對象,則需要新的堆空間谁撼。

放心歧胁,關(guān)于字符串常量池,后面有時間的話彤敛,我再單獨(dú)寫一篇文章詳細(xì)地說一說与帆。

2)安全性

字符串在 Java 應(yīng)用程序中的使用范圍非常廣,幾乎無處不在墨榄,比如說存儲用戶名玄糟、密碼、數(shù)據(jù)庫連接地址等等這些非常敏感的信息袄秩,因此阵翎,必須要保證 String 類的絕對安全性。

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

void criticalMethod(String userName) {
    // 檢查用戶名是否合法
    if (!isAlphaNumeric(userName)) {
        throw new SecurityException(); 
    }
     
    // 初始化數(shù)據(jù)庫連接
    initializeDatabase();
     
    // 準(zhǔn)備修改用戶狀態(tài)
    connection.executeUpdate("UPDATE members SET status = 'active' " +
      " WHERE username = '" + userName + "'");
}

通常情況下之剧,用戶名由客戶端傳遞到服務(wù)器端郭卫,服務(wù)器端接收后要先對用戶名進(jìn)行檢查,再進(jìn)行其他操作背稼,因?yàn)榭蛻舳藗鬟f過來的信息不一定值得信任贰军。

如果字符串是可變的,那么我們在執(zhí)行 executeUpdate 更新數(shù)據(jù)庫的時候蟹肘,就有點(diǎn)不放心词疼,因?yàn)榧幢闶前踩詸z查通過了,字符串仍然有可能被修改帘腹。

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

但如果字符串是不可變的舵盈,這方面的擔(dān)憂就不存在了陋率。因?yàn)樵趫?zhí)行更新之前,字符串的值是確定的秽晚,就是我們檢查安全性之后的值瓦糟。

3)線程安全

由于字符串是不可變的,因此可以在多線程之間共享赴蝇,如果一個線程把字符串的值修改為另外一個狸页,那么就會在字符串常量池中創(chuàng)建另外一個字符串,原有的字符串仍然會保持不變扯再。

不過芍耘,很遺憾,我還不知道怎么從代碼層面上去證明這一點(diǎn)熄阻,只能純理論 yy 一下斋竞。小伙伴誰有辦法的,教教我秃殉,在線等的那種坝初。

4)哈希碼

字符串廣泛應(yīng)用于 HashMap、HashTable钾军、HashSet 等需要哈希碼作為鍵的數(shù)據(jù)結(jié)構(gòu)中鳄袍,在對這些哈希表進(jìn)行操作的時候,需要頻繁調(diào)用 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);
}

由于字符串是不可變性拗小,這就保證了鍵值的哈希值不會發(fā)生改變,因此在第一次調(diào)用 String 類的 hashCode() 方法時樱哼,就對哈希值進(jìn)行了緩存哀九,此后,就一直返回相同的值搅幅。

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

由于哈希值被緩存了阅束,這在另外一種層面上提高了哈希表的訪問性能,因?yàn)楣V挡挥弥匦掠嬎懔恕?/p>

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

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

03常遂、總結(jié)

好了纳令,我親愛的小伙伴們,以上就是本文的全部內(nèi)容了。我相信你一定對字符串的不可變性有了充足的了解平绩,由于字符串是不可變的圈匆,因此我們可以將它看作是一個特殊的基本數(shù)據(jù)類型,哪怕是在多線程的環(huán)境下捏雌,也不用擔(dān)心它的值是否會發(fā)生改變跃赚。

如果覺得文章對你有點(diǎn)幫助,請微信搜索「 沉默王二 」第一時間閱讀性湿。

本文已收錄 GitHub纬傲,傳送門~ ,里面更有大廠面試完整考點(diǎn)肤频,歡迎 Star叹括。

我是沉默王二,一枚有顏值卻靠才華茍且的程序員宵荒。關(guān)注即可提升學(xué)習(xí)效率汁雷,別忘了三連啊,點(diǎn)贊报咳、收藏侠讯、留言,我不挑暑刃,嘻嘻厢漩。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市岩臣,隨后出現(xiàn)的幾起案子袁翁,更是在濱河造成了極大的恐慌,老刑警劉巖婿脸,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件粱胜,死亡現(xiàn)場離奇詭異,居然都是意外死亡狐树,警方通過查閱死者的電腦和手機(jī)焙压,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來抑钟,“玉大人涯曲,你說我怎么就攤上這事≡谒” “怎么了幻件?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長蛔溃。 經(jīng)常有香客問我绰沥,道長篱蝇,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任徽曲,我火速辦了婚禮零截,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘秃臣。我一直安慰自己涧衙,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布奥此。 她就那樣靜靜地躺著弧哎,像睡著了一般。 火紅的嫁衣襯著肌膚如雪稚虎。 梳的紋絲不亂的頭發(fā)上傻铣,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天,我揣著相機(jī)與錄音祥绞,去河邊找鬼非洲。 笑死,一個胖子當(dāng)著我的面吹牛蜕径,可吹牛的內(nèi)容都是我干的两踏。 我是一名探鬼主播,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼兜喻,長吁一口氣:“原來是場噩夢啊……” “哼梦染!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起朴皆,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤帕识,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后遂铡,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體肮疗,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年扒接,在試婚紗的時候發(fā)現(xiàn)自己被綠了伪货。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡钾怔,死狀恐怖碱呼,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情宗侦,我是刑警寧澤愚臀,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站矾利,受9級特大地震影響姑裂,放射性物質(zhì)發(fā)生泄漏馋袜。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一炭分、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧剑肯,春花似錦捧毛、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至溃睹,卻和暖如春而账,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背因篇。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工泞辐, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人竞滓。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓咐吼,卻偏偏與公主長得像,于是被迫代替她去往敵國和親商佑。 傳聞我的和親對象是個殘疾皇子锯茄,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評論 2 345