Java字符串原理分析之String是否可變
一. Spring源碼中的final關(guān)鍵詞
為了弄清楚String為什么具有不可變性,我們先來看看String的源碼,尤其是源碼中帶有final關(guān)鍵詞的地方。
1. final的特點(diǎn)
為了更好地理解String相關(guān)的內(nèi)容,在閱讀String源碼之前,我們先來復(fù)習(xí)一下final關(guān)鍵詞有哪些特點(diǎn),因?yàn)樵赟tring中會(huì)涉及到很多final相關(guān)的內(nèi)容。
final關(guān)鍵詞修飾的類不可以被其他類繼承,但是該類本身可以繼承其他類,通俗的說就是這個(gè)類可以有父類,但是不能有子類;
final關(guān)鍵詞修飾的方法不可以被覆蓋重寫,但是可以被繼承使用;
final關(guān)鍵詞修飾的基本數(shù)據(jù)類型變量稱為常量,只能被賦值一次;
final關(guān)鍵詞修飾的引用數(shù)據(jù)類型的變量值為地址值,地址值不能改變,但是地址內(nèi)的數(shù)據(jù)對(duì)象可以被改變;
final關(guān)鍵詞修飾的成員變量,需要在創(chuàng)建對(duì)象前賦值,否則會(huì)報(bào)錯(cuò)(即需要在定義時(shí)直接賦值,如果是在構(gòu)造方法中賦值,則多個(gè)構(gòu)造方法均需賦值)。
復(fù)習(xí)了final的特點(diǎn)之后,接下來我們就可以閱讀String的源碼了。
2. String源碼解讀
接下來就請(qǐng)大家請(qǐng)跟著小編來看看String源碼中關(guān)于不可變性的內(nèi)容吧。
2.1 final修飾的String類
/** * ......其他略...... * * Strings are constant; their values cannot be changed after they * are created. String buffers support mutable strings. * Because String objects are immutable they can be shared. For example: * * ......其他略...... */ public final class String implements java.io.Serializable, Comparable<String>, CharSequence { ......
先對(duì)上面的源碼及其注釋進(jìn)行簡(jiǎn)單的解釋:
- final:請(qǐng)參考第1小節(jié)對(duì)final特點(diǎn)的介紹;
- Serializable:用于序列化;
- Comparable:默認(rèn)的比較器;
- CharSequence: 提供對(duì)字符序列進(jìn)行統(tǒng)一、只讀的操作。
從這一段源碼及注釋中,我們可以得出如下結(jié)論:
- String類用final關(guān)鍵字修飾,說明String不可被繼承;
- String字符串是常量,字符串的值一旦被創(chuàng)建,就不能被改變;
- String字符串緩沖區(qū)支持可變字符串;
- 因?yàn)镾tring對(duì)象是不可變的,所以它們是可以被共享的。
2.2 final修飾的value[]屬性
public final class String implements java.io.Serializable, Comparable<String>, CharSequence { /** The value is used for character storage. */ private final char value[]; ......
從源碼中可以看出,value[]是一個(gè)私有的字符數(shù)組,String類其實(shí)就是通過這個(gè)char數(shù)組來保存字符串內(nèi)容的。簡(jiǎn)單的說,我們定義的字符串會(huì)被拆成一個(gè)一個(gè)的字符,這些字符都被存放在這個(gè)value字符數(shù)組里面。
這里的value[]數(shù)組被final修飾,初始化之后就不能再被更改。但是大家注意,我們這里說的value[]不可變,指的是value的引用地址不可變,但是value數(shù)組里面的數(shù)據(jù)元素其實(shí)是可變的! 這是因?yàn)関alue是數(shù)組類型,根據(jù)我們之前學(xué)過的知識(shí),value的引用地址會(huì)分配在棧中 ,而其對(duì)應(yīng)的數(shù)據(jù)是在常量池中保存的。所以我們說String不可變,指的就是value在棧中的引用地址不可變,而不是說常量池中數(shù)組本身的數(shù)據(jù)元素不可變。
另外我們要注意,Java中的字符串常量池,用來存儲(chǔ)字符串字面量! 但是由于JDK版本的不同,常量池的位置也不同:
JDK 6 及以下版本的字符串常量池是在方法區(qū)(Perm Gen)中,此時(shí)常量池中存儲(chǔ)的是字符串對(duì)象;在 JDK 8.0 中,方法區(qū)(永久代被元空間取代了;
JDK 7、8以后的字符串常量池被轉(zhuǎn)移到了堆中,此時(shí)常量池存儲(chǔ)的就是字符串對(duì)象的引用,而不是字符串對(duì)象本身。
至此,就帶各位把String類中的核心源碼分析完了,接下來我們?cè)龠M(jìn)一步分析String不可變的原因,及其他底層原理設(shè)計(jì)。
二. String的不可變性
1. 實(shí)驗(yàn)案例
了解了上面的這些核心源碼之后,接下來再帶各位來驗(yàn)證一下,看看String到底能不能變!我先給各位來一段案例代碼,代碼案例如下圖所示。
結(jié)果s的內(nèi)容變了,好像是啪啪打臉了????。?!咋回事,不是說了String不可變嗎?怎么這么快就翻車打臉了?別急,讓我們好好來分析一下。
2. 結(jié)果剖析
首先我們從結(jié)果上來看String s 變量的結(jié)果好像改變了,但為什么我們又說String是不可變的呢?
要想明白這個(gè)問題,我們得先弄清楚一個(gè)點(diǎn),即引用和值的區(qū)別!在上面的代碼中,我們先是創(chuàng)建了一個(gè) "yiyige" 為內(nèi)容的字符串引用s,s其實(shí)先是指向了value對(duì)象,而value對(duì)象則指向存儲(chǔ)了 "y,i,y,i,g,e" 字符的字符數(shù)組。因?yàn)関alue被final修飾,所以value的值不可被更改。因此,上面代碼中改變的其實(shí)是s的引用指向,而不是改變了String對(duì)象的值 。 換句話說,上面實(shí)例中 s的值 只是 value的引用地址,并不是String內(nèi)容本身!當(dāng)我們執(zhí)行 s = "yyg"; 語句時(shí),Java中會(huì)創(chuàng)建一個(gè)新的字面量對(duì)象 "yyg",而原來的 "yiyige" 字面量對(duì)象依然存在于內(nèi)存的intern緩存池中。 在Java中,因?yàn)閿?shù)組也是對(duì)象, 所以value中存儲(chǔ)的也只是一個(gè)引用,它指向一個(gè)真正的數(shù)組對(duì)象。在執(zhí)行了String s = “yiyige”; 這句代碼之后,真正的內(nèi)存布局應(yīng)該是下圖這樣的:
因?yàn)関alue是String封裝的字符數(shù)組,value中的所有字符都屬于String這個(gè)對(duì)象。由于value是private的,且沒有提供setValue等公共方法來修改這個(gè)value值,所以在String類的外部是無法修改value值的,也就是說一旦初始化就不能被修改。此外,value變量是final的, 也就是說在String類內(nèi)部,一旦這個(gè)值初始化了,value這個(gè)變量所引用的地址就不會(huì)改變了,即一直引用同一個(gè)對(duì)象。正是基于這一層,所以說String對(duì)象是不可變的對(duì)象。 但其實(shí)value所引用對(duì)象的內(nèi)容完全可以發(fā)生改變,我們可以利用反射來消除String類對(duì)象的不可變特性 。
所以String的不可變性,指的是value在棧中的引用地址不可變,而不是說常量池中array本身的數(shù)據(jù)元素不可變!
而String對(duì)象的改變實(shí)際上是通過內(nèi)存地址的 “斷開-連接” 變化來完成的,這個(gè)過程中原字符串中的內(nèi)容并沒有任何的改變。String s = "yiyige"; 和 s = "yyg"; 實(shí)質(zhì)上是開辟了2個(gè)內(nèi)存空間,s 只是由原來指向 "yiyige" 變?yōu)橹赶?"yyg" 而已,而其原來的字符串內(nèi)容,是沒有改變的,如下圖所示。
因此,我們?cè)谝院蟮拈_發(fā)中,如果要經(jīng)常修改字符串的內(nèi)容,請(qǐng)盡量少用String,因?yàn)樽址闹赶?ldquo;斷開-連接”會(huì)大大降低性能,建議使用:StringBuilder、StringBuffer。
那么String一定不可變嗎?有沒有辦法讓String真的可變呢?我們繼續(xù)往下學(xué)習(xí)!
三. String真的不可變嗎?
1. 實(shí)驗(yàn)案例
我在前面的章節(jié)中給大家說,String的不可變,其實(shí)指的是String類中value屬性在棧中的引用地址不可變,而不是說常量池中array本身的數(shù)據(jù)元素不可變!也就是說String字符串的內(nèi)容其實(shí)是可變的!那怎么實(shí)現(xiàn)呢?利用反射就可以實(shí)現(xiàn),我們通過一個(gè)案例來證明一下。
try { String str = "yyg"; System.out.println("str=" + str + ", 唯一性hash值=" + System.identityHashCode(str)); Class stringClass = str.getClass(); //獲取String類中的value屬性 Field field = stringClass.getDeclaredField("value"); //設(shè)置私有成員的可訪問性,進(jìn)行暴力反射 field.setAccessible(true); //獲取value數(shù)組中的內(nèi)容 char[] value = (char[]) field.get(str); System.out.println("value=" + Arrays.toString(value)); value[1] = 'z'; System.out.println("str=" + str + ", 唯一性hash值=" + System.identityHashCode(str)); } catch (NoSuchFieldException | IllegalAccessException e) { e.printStackTrace(); }
2. 結(jié)果剖析
上面案例的執(zhí)行結(jié)果如下圖所示:
我們可以看到,String字符串的字符數(shù)組可以通過反射進(jìn)行修改,導(dǎo)致字符串的“內(nèi)容”真的發(fā)生了變化! 并且我們又利用底層的java.lang.System#identityHashCode()方法(不管是否重寫了hashCode方法)獲取了對(duì)象的唯一哈希值,該方法獲取的hash值與hashCode()方法是一樣的。我們可以看到兩個(gè)字符串的唯一性hash值是一樣的,證明字符串引用地址沒有發(fā)生改變!所以在這里,我們并不是像之前那樣創(chuàng)建了一個(gè)新的String字符串,而是真的改變了String的內(nèi)容。這個(gè)代碼案例進(jìn)一步說明,String類的不可變指的是中value屬性在棧中的引用地址不可變,而不是說常量池中array本身的數(shù)據(jù)元素不可變!也就是說String字符串的內(nèi)容其實(shí)是可變的!
四. 結(jié)語
String作為Java中使用最為廣泛的一個(gè)類,之所以設(shè)計(jì)為不可變,主要是出于效率與安全性方面考慮。這種設(shè)計(jì)有優(yōu)點(diǎn),也有缺點(diǎn)。
1. 不可變性的優(yōu)點(diǎn)
只有當(dāng)字符串是不可變的,字符串池才有可能實(shí)現(xiàn)。 字符串池的實(shí)現(xiàn)可以在運(yùn)行時(shí)節(jié)約很多heap空間,因?yàn)椴煌淖址枚伎梢灾赶虺刂械耐粋€(gè)字符串。但如果字符串是可變的,如果一個(gè)引用變量改變了字符串的值,那么其它指向這個(gè)值的變量內(nèi)容也會(huì)跟著一起改變。
如果字符串是可變的,那么可能會(huì)引起很嚴(yán)重的安全問題。 譬如,數(shù)據(jù)庫的用戶名、密碼都是以字符串的形式傳入數(shù)據(jù)庫,以獲得數(shù)據(jù)庫的連接;或者在socket編程中,主機(jī)名和端口都是以字符串的形式傳入。因?yàn)樽址遣豢勺兊?,所以它的值是不可改變的,否則黑客們可以鉆到空子,改變字符串指向的對(duì)象值,造成安全漏洞。
因?yàn)樽址遣豢勺兊模?/strong> 在物理上是絕對(duì)的線程安全 ,所以同一個(gè)字符串實(shí)例可以被多個(gè)線程共享。 由于不可變對(duì)象不可能被修改,因此能夠在多線程中被任意自由訪問而不導(dǎo)致線程安全問題,不需要多余的同步操作。即在并發(fā)場(chǎng)景下,多個(gè)線程同時(shí)讀一個(gè)資源,并不會(huì)引發(fā)競(jìng)態(tài)條件,只有對(duì)資源進(jìn)行讀寫才有危險(xiǎn)。不可變對(duì)象不能被寫,所以線程安全。
類加載器要用到字符串,不可變性也提供了安全性,以便正確的類可以被加載。 譬如你想加載java.sql.Connection類,而這個(gè)值被改成了myhacked.Connection,那么會(huì)對(duì)你的數(shù)據(jù)庫造成不可知的破壞。
因?yàn)樽址遣豢勺兊模栽谧址畬?duì)象創(chuàng)建的時(shí)候hashCode()就被執(zhí)行并把執(zhí)行結(jié)果緩存了,不需要重新計(jì)算。這就使得字符串很適合作為Map中的鍵,所以字符串的處理速度要快過其它的鍵對(duì)象,這就是HashMap中的鍵往往都使用字符串的原因,當(dāng)我們需要頻繁讀取訪問任意鍵值對(duì)時(shí),能夠節(jié)省很多的CPU計(jì)算開銷。
Sting的不可變性會(huì)提高執(zhí)行性能和效率,基于Sting不可變,我們就可以用緩存池將String對(duì)象緩存起來,同時(shí)把一個(gè)String對(duì)象的地址賦值給多個(gè)String引用,這樣可以安全保證多個(gè)變量共享同一個(gè)對(duì)象。因此,構(gòu)造一萬個(gè)string s = "xyz",實(shí)際上得到都是同一個(gè)字符串對(duì)象,避免了很多不必要的空間開銷。
2. 不可變性的缺點(diǎn)
- 喪失了部分靈活性。我們平時(shí)使用的大部分都是可變對(duì)象,比如內(nèi)容變化時(shí),只需要利用setValue()更新一下就可以了,不需要重新創(chuàng)建一個(gè)對(duì)象,但是String很難做到這一點(diǎn)。當(dāng)然,我們完全可以使用StringBuilder來彌補(bǔ)這個(gè)缺點(diǎn)。
- 脆弱的不可變性,String其實(shí)可以利用JNI或反射來改變其不可變性。
以上就是Java字符串原理分析之String是否可變的詳細(xì)內(nèi)容,更多關(guān)于Java String原理的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Spring Boot集成Spring Cloud Security進(jìn)行安全增強(qiáng)的方法
Spring Cloud Security是Spring Security的擴(kuò)展,它提供了對(duì)Spring Cloud體系中的服務(wù)認(rèn)證和授權(quán)的支持,包括OAuth2、JWT等,這篇文章主要介紹了Spring Boot集成Spring Cloud Security進(jìn)行安全增強(qiáng),需要的朋友可以參考下2024-11-11MySQL?MyBatis?默認(rèn)插入當(dāng)前時(shí)間方式
這篇文章主要介紹了MySQL?MyBatis?默認(rèn)插入當(dāng)前時(shí)間方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-10-10springboot實(shí)現(xiàn)全局異常捕獲的使用示例
任何系統(tǒng),我們不會(huì)傻傻的在每一個(gè)地方進(jìn)行異常捕獲和處理,整個(gè)系統(tǒng)一般我們會(huì)在一個(gè)的地方統(tǒng)一進(jìn)行異常處理,本文主要介紹了springboot實(shí)現(xiàn)全局異常捕獲的使用示例,感興趣的可以了解一下2023-11-11Java開發(fā)學(xué)習(xí)之Bean的作用域和生命周期詳解
這篇文章主要介紹了淺談Spring中Bean的作用域,生命周期和注解,從創(chuàng)建到消亡的完整過程,例如人從出生到死亡的整個(gè)過程就是一個(gè)生命周期。本文將通過示例為大家詳細(xì)講講,感興趣的可以學(xué)習(xí)一下2022-06-06