中國(guó)建設(shè)銀行新疆分行網(wǎng)站移動(dòng)建站優(yōu)化
其實(shí)在Java中,String類(lèi)被final修飾,主要是為了保證字符串的不可變性,進(jìn)而保證了它的安全性。那么final到底是怎么保證字符串安全性的呢?接下來(lái)就讓我們一起來(lái)看看吧。
一. final的作用
1.??final關(guān)鍵詞修飾的類(lèi)不可以被其他類(lèi)繼承,但是該類(lèi)本身可以繼承其他類(lèi),通俗地說(shuō)就是這個(gè)類(lèi)可以有父類(lèi),但不能有子類(lèi)。
1 2 3 |
|
2.??final關(guān)鍵詞修飾的方法不可以被覆蓋重寫(xiě),但可以被繼承使用。
1 2 3 4 5 |
|
3.??final關(guān)鍵詞修飾的基本數(shù)據(jù)類(lèi)型被稱(chēng)為常量,只能被賦值一次?! ?/p>
1 2 3 |
|
4.??final關(guān)鍵詞修飾的引用數(shù)據(jù)類(lèi)型變量,其值為地址值,該地址值不能改變,但該地址對(duì)應(yīng)的數(shù)據(jù)對(duì)象可以被改變(其實(shí)這一點(diǎn)就和我們今天要說(shuō)的內(nèi)容有關(guān)了,在后面我會(huì)結(jié)合案例跟大家重點(diǎn)解釋,大家一定要打起精神仔細(xì)學(xué)習(xí)哦)。
5.??final關(guān)鍵詞修飾的成員變量,需要在創(chuàng)建對(duì)象前就賦值,否則會(huì)報(bào)錯(cuò)(即需要在定義時(shí)直接賦值)。
綜上所述,我們可以知道,final在Java中是一個(gè)非常有用的關(guān)鍵字,主要可以提高我們代碼的穩(wěn)定性和可讀性。當(dāng)然,我們今天要講解的重點(diǎn)是被final修飾的String類(lèi),所以接下來(lái)我們還是把目光轉(zhuǎn)回到String身上來(lái),看看String都有哪些特性吧!
二. 被final修飾的String類(lèi)
為了讓大家更好地理解String的不可變性,首先我要給各位簡(jiǎn)要地講一下String的源碼設(shè)計(jì)。從下面的這段源碼中,我們可以搞清楚很多底層的設(shè)計(jì)思路,接下來(lái)就請(qǐng)大家跟著我一起來(lái)看看String的核心源碼吧。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
我先把上面的源碼及其注釋,給大家作一個(gè)簡(jiǎn)單的解釋:
●?final:請(qǐng)參考第1小節(jié)對(duì)final特點(diǎn)的介紹;
●?Serializable:用于序列化;
●?Comparable<String>:默認(rèn)的比較器;
●?CharSequence: 提供對(duì)字符序列進(jìn)行統(tǒng)一、只讀的操作。
從這段源碼及其注釋中,我們可以得到下面這些結(jié)論:
●?String類(lèi)用final關(guān)鍵字修飾,說(shuō)明String不可被繼承;
●?String字符串是常量,字符串的值一旦被創(chuàng)建,就不能被改變;
●?String字符串緩沖區(qū)支持可變字符串;
●?String對(duì)象是不可變的,它們是可以被共享的。
三. String的不可變性
在學(xué)習(xí)了上面的這些核心源碼之后,接下來(lái),我們可以通過(guò)一個(gè)案例來(lái)實(shí)踐驗(yàn)證一番,看看String字符串的內(nèi)容到底能不能改變。這里有個(gè)代碼案例,如下圖所示:
在上述的案例結(jié)果中,大家可以看出,s的內(nèi)容竟然發(fā)生了改變?!但我們不是一直說(shuō)String是不可變的嗎?這是咋回事?大家先別急,我們繼續(xù)往下看。
要想弄明白這個(gè)問(wèn)題,我們首先得知道一個(gè)知識(shí)點(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ì)象的值!
換句話說(shuō),上面實(shí)例中s的值,其實(shí)只是value的引用地址,并不是String的內(nèi)容本身。當(dāng)我們執(zhí)行 s = "yyg"?語(yǔ)句時(shí),Java會(huì)創(chuàng)建一個(gè)新的字面量對(duì)象 "yyg",而原來(lái)的 "yiyige" 字面量對(duì)象其實(shí)依然存在于內(nèi)存的intern緩存池中。
在這里,String對(duì)象的改變,實(shí)際上是通過(guò)內(nèi)存地址的“斷開(kāi)-連接”變化來(lái)完成的。在這個(gè)過(guò)程中,原字符串中的內(nèi)容并沒(méi)有發(fā)生任何的改變。String s = "yiyige"?和 s = "yyg"這兩行代碼,實(shí)質(zhì)上是開(kāi)辟了2個(gè)內(nèi)存空間,s只是由原來(lái)指向 "yiyige" 變?yōu)橹赶?"yyg" 而已,而其原來(lái)的字符串內(nèi)容,是沒(méi)有發(fā)生改變的,如下圖所示。
因此,我們?cè)谝院蟮拈_(kāi)發(fā)中,如果要經(jīng)常修改字符串的內(nèi)容,請(qǐng)盡量少用String!因?yàn)槿绻址闹赶蚪?jīng)常的“斷開(kāi)-連接”,就會(huì)大大降低性能,我建議大家使用StringBuilder?或?StringBuffer?進(jìn)行替換。
我們繼續(xù)把上面的代碼深入地分析一下。在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的,沒(méi)有提供setValue等公共方法來(lái)修改這個(gè)value值,所以我們?cè)赟tring類(lèi)的外部是無(wú)法修改value值的,也就是說(shuō)字符串一旦初始化就不能再被修改。
此外,value變量是final修飾的,也就是說(shuō)在String類(lèi)內(nèi)部,一旦這個(gè)值初始化了,value這個(gè)變量所引用的地址就不會(huì)改變了,即一直引用同一個(gè)對(duì)象。正是基于這一層,我們才說(shuō)String對(duì)象是不可變的對(duì)象。
所以String的不可變,其實(shí)是指value在棧中的引用地址不可變,而不是說(shuō)常量池中value字符數(shù)組里的數(shù)據(jù)元素不可變。也就是說(shuō),value所引用的數(shù)組對(duì)象里的內(nèi)容,其實(shí)是可以發(fā)生改變的。
那么我們又如何改變它呢?這就要通過(guò)反射來(lái)消除String類(lèi)對(duì)象的不可變性啦!
四. String真的不可變嗎?
在上述內(nèi)容中,我們重點(diǎn)給大家解釋了String字符串的可變性。現(xiàn)在大家應(yīng)該已經(jīng)知道了,String字符串的內(nèi)容其實(shí)是可變的,不可改變的只是String字符串的對(duì)象地址。那么我們到底該怎么讓String字符串的內(nèi)容發(fā)生改變呢?在上述我們給大家提到了反射,接下來(lái)我們就來(lái)看看如何通過(guò)反射改變String字符串的內(nèi)容吧。代碼案例如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
執(zhí)行結(jié)果如下圖所示:
??
從上面的結(jié)果中我們可以看到,String字符串的字符數(shù)組,通過(guò)反射進(jìn)行修改后,字符串的“內(nèi)容”真的發(fā)生了變化!
并且我們又利用底層的java.lang.System#identityHashCode()方法(不管是否重寫(xiě)了hashCode方法),來(lái)獲取到了該字符串對(duì)象的唯一哈希值,該方法獲取的hash值與hashCode()方法是一樣的。
從結(jié)果中,我們可以看到兩個(gè)字符串的唯一hash值是一樣的,這就證明字符串的引用地址沒(méi)有發(fā)生改變。
所以這就說(shuō)明,我們并不是像之前那樣創(chuàng)建了一個(gè)新的String字符串,而是真的改變了原有String的內(nèi)容。
這個(gè)代碼案例進(jìn)一步證明了我們上面的結(jié)論:String字符串的不可變,指的其實(shí)是value對(duì)象在棧中的引用地址不可變,而不是說(shuō)常量池中value里的數(shù)據(jù)元素不可變!簡(jiǎn)單地說(shuō),就是String字符串的內(nèi)容其實(shí)是可以改變的,不能改表的是它的對(duì)象地址而已。
?所以這也就是我們上述所說(shuō)的final的作用之一:final關(guān)鍵詞修飾的引用數(shù)據(jù)類(lèi)型的變量,其值為地址值,地址值不能改變,但是地址內(nèi)的數(shù)據(jù)對(duì)象可以被改變!
五. 總結(jié)
至此,我們就把今天的面試題分析完了,現(xiàn)在你明白了嗎?最后我再來(lái)給大家總結(jié)一下今天的重點(diǎn)內(nèi)容吧:
1.??為什么要用final修飾java中的String類(lèi)呢?
核心:因?yàn)樗_保了字符串的安全性和可靠性。
2.??java中的String真的不可變嗎?
核心:String字符串的內(nèi)容其實(shí)是可變的,但要通過(guò)特殊手段進(jìn)行實(shí)現(xiàn),不可改變的是String字符串對(duì)象的地址。
3.??如何消除String類(lèi)對(duì)象的不可變性?
核心:利用反射來(lái)消除String類(lèi)對(duì)象的不可變性。
4.??如果想要保證String的不可變要注意哪些?
●?首先,將 String 類(lèi)聲明為 final類(lèi)型。這意味著String類(lèi)是不可被繼承的,防止程序員通過(guò)繼承重寫(xiě)String類(lèi)的某些方法,使得String類(lèi)出現(xiàn)“可變的”的情況;
●?然后,重要的字符數(shù)組value屬性,要被private 和 final修飾。它是String的底層數(shù)組,用于存貯字符串內(nèi)容。又因?yàn)閿?shù)組是引用類(lèi)型,所以只能限制引用不被改變,也就是說(shuō)數(shù)組元素的值是可以改變的,這在上面的案例中已經(jīng)證明過(guò)了;
●?接著,所有修改的方法都返回新的字符串對(duì)象,保證修改時(shí)不會(huì)改變?cè)紝?duì)象的引用;
●?最后,不同的字符串對(duì)象都可以指向緩存池中的同一個(gè)字符串字面量。
當(dāng)然,“Java中的String類(lèi)使用final修飾”這個(gè)概念非常重要,因?yàn)樗_保了字符串的安全性和可靠性。但是我們也要清楚不可改變的只是它的地址,而不是它的內(nèi)容,它的內(nèi)容是可以利用反射來(lái)改變的!只不過(guò)在一般的描述中,大家都會(huì)說(shuō)String內(nèi)容不可改變,畢竟很多時(shí)候是不允許利用反射這種特殊的功能去進(jìn)行這樣的操作的。