深入理解java.lang.String類的不可變性
1. 字符串 String 的不可變性
什么是不可變類?
這樣理解:
一個對象在創(chuàng)建完成后,不能去改變它的狀態(tài),不能改變它的成員變量(如果成員變量包含基本數(shù)據(jù)類型,那么這個基本數(shù)據(jù)類型的值不能改變;如果包含引用類型,那么這個引用類型的變量不能指向別的對象)
不可變類只是其實例不能被修改的類。每個實例中包含的所有信息都必須在創(chuàng)建該實例的時候就提供,并且在對象的整個生命周期內(nèi)固定不變。為了使類不可變,要遵循下面五條規(guī)則:
- 不要提供任何會修改對象狀態(tài)的方法
- 保證類不會被擴(kuò)展。 一般的做法是讓這個類稱為 final 的,防止子類化,破壞該類的不可變行為
- 使所有的域都是 final 的
- 使所有的域都成為私有的。 防止客戶端獲得訪問被域引用的可變對象的權(quán)限,并防止客戶端直接修改這些對象
- 確保對于任何可變性組件的互斥訪問。 如果類具有指向可變對象的域,則必須確保該類的客戶端無法獲得指向這些對象的引用
翻閱 API 文檔:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
// value 數(shù)組被 final 修飾
private final char value[];
...
}
String 類代表字符串。Java 程序中的所有字符串字面值(如 “abc” )都作為此類的實例來實現(xiàn)。 這些字面值都是直接存儲在“方法區(qū)”的 字符串常量池 中
字符串是常量;它們的值在創(chuàng)建之后 不能改變,所以可以共享它們。例如:
String str = "abc";
這時就有人疑惑了:為什么 String 不可變?但我的代碼中經(jīng)常改變 String 啊,如下:
String str = "HELLO"; str = "WORLD"; System.out.println(str); // WORLD
這樣操作,不就是將 “HELLO” 對象改變成了 “WORLD” 對象了嗎?
雖然字符串的內(nèi)容看上去從“HELLO” 變成了“WORLD”,但實際上,這已經(jīng)是生成了一個新的字符串了:
String str = "HELLO"; System.out.println(str.hashCode()); // 68624562 str = "WORLD"; System.out.println(str.hashCode()); // 82781042
變量 str 前后的 hashCode 值不一樣,說明了 str 在改變前后,指向了不同的對象。所以,變量 str 只是指向了不同對象,字符串 “HELLO”對象本身沒有被改變。
變量 str 的指向如下圖所示(jdk1.8:字符串常量位于堆中):

我們也可以使用 javap 命令來查看 class 的常量池:
javap -c -v StringTest.class
執(zhí)行后,常量池信息如下:

從常量池中可以看出,確實有兩個字符串對象:HELLO、WORLD
【總結(jié)】:一旦一個 String 對象堆中被創(chuàng)建出來,它就無法被修改。而且,String 類的所有 API 方法都沒有改變字符串本身的值,都是返回了一個新的字符串對象。
2. String 設(shè)計成不可變類的好處
在了解了“String 是不可變”的之后,大家是不是很疑惑:為什么要把 String 設(shè)計成不可變的呢?這樣做又有什么好處呢?
主要從以下幾個角度考慮:
- 安全可靠性:字符串在 Java 應(yīng)用程序中應(yīng)用廣泛(存儲敏感信息,如:用戶名、密碼、連接 url、網(wǎng)絡(luò)連接等);JVM類加載器在加載類的時也廣泛地使用它。因此,保護(hù) String 類對于提升整個應(yīng)用程序的安全性至關(guān)重要。
- 緩存:字符串是使用最廣泛的數(shù)據(jù)結(jié)構(gòu),大量的字符串的創(chuàng)建是非常耗費資源的。JVM 中專門開辟了一部分空間來存儲 Java 字符串,那就是字符串常量池。通過字符串常量池,兩個內(nèi)容相同的字符串變量,可以從池中指向同一個字符串對象,從而節(jié)省了關(guān)鍵的內(nèi)存資源
- 線程安全:不可變會自動使字符串成為線程安全的,因為當(dāng)從多個線程訪問它們時,它們不會被更改
- hashcode 緩存:字符串也被廣泛地用于哈希實現(xiàn),如 HashMap、HashTable、HashSet 等。在對這些散列實現(xiàn)進(jìn)行操作時,經(jīng)常調(diào)用鍵的hashCode() 方法。不可變性保證了字符串的值不會改變,因此,hashCode() 方法在 String 類中被重寫,以方便緩存。這樣,在第一次hashCode() 調(diào)用期間計算和緩存散列,并從那時起返回相同的值。
3. 面試題
// 生成兩個對象:一個在常量池中;一個中堆中,且都是 hello 對象
String s = new String("hello");
那么,下面會生成幾個對象呢?
// 只會在字符串常量池中生成一個對象:helloworld。 String s3 = "hello" + "world";
這種字面量用“+”拼接,編譯器在編譯期間會直接進(jìn)行優(yōu)化。
// 這個會生成4個對象。2個在常量池中:hello、world // 2個在堆中:StringBuilder、helloworld對象 String s = "hello"; String s2 = s + "world";
編譯后,使用反編譯軟件 ------ jad 進(jìn)行查看:
String s1 = "hell0";
String s2 = (new StringBuilder()).append(s1).append("world").toString();
發(fā)現(xiàn):使用“+”將變量和字面量進(jìn)行拼接的結(jié)果是:將 String 轉(zhuǎn)成了StringBuilder 后,使用其 append() 方法進(jìn)行處理的
查看 StringBuilder.toString() 方法源碼:
@Override
public String toString() {
// char[] value; value 是 StringBuilder 類的成員變量
return new String(value, 0, count);
}
最后調(diào)用 toString() 方法時,會創(chuàng)建一個 String 對象。這個字符串對象只會在堆中創(chuàng)建,并不會在字符串常量池中創(chuàng)建。所以,會創(chuàng)建4個對象(hello 和 world 會直接在字符串常量池中創(chuàng)建)。
到此這篇關(guān)于深入理解java.lang.String類的不可變性的文章就介紹到這了,更多相關(guān)java.lang.String不可變性內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot項目中使用OkHttp獲取IP地址的示例代碼
OkHttp?是一個由?Square?開發(fā)的高效、現(xiàn)代的?HTTP?客戶端庫,用于?Android?和?Java?應(yīng)用程序,它支持?HTTP/2?和?SPDY?等現(xiàn)代網(wǎng)絡(luò)協(xié)議,并提供了多種功能和優(yōu)化,本文給大家介紹了SpringBoot項目中如何獲取IP地址,需要的朋友可以參考下2024-08-08
springboot 運行 jar 包讀取外部配置文件的問題
這篇文章主要介紹了springboot 運行 jar 包讀取外部配置文件,本文主要描述linux系統(tǒng)執(zhí)行jar包讀取jar包同級目錄的外部配置文件,主要分為兩種方法,每種方法通過實例代碼介紹的非常詳細(xì),需要的朋友可以參考下2021-07-07
java多線程之并發(fā)工具類CountDownLatch,CyclicBarrier和Semaphore
這篇文章主要為大家介紹了java并發(fā)工具類CountDownLatch,CyclicBarrier和Semaphore ,具有一定的參考價值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來幫助2021-12-12
基于JavaMail的Java實現(xiàn)復(fù)雜郵件發(fā)送功能
這篇文章主要為大家詳細(xì)介紹了基于JavaMail的Java實現(xiàn)復(fù)雜郵件發(fā)送功能,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-09-09

