String很多實(shí)用的特性褪子,比如說(shuō)“不可變性”量淌,是工程師精心設(shè)計(jì)的藝術(shù)品!藝術(shù)品易碎嫌褪!用final就是拒絕繼承呀枢,防止世界被熊孩子破壞,維護(hù)世界和平笼痛!
1. 什么是不可變裙秋?
String不可變很簡(jiǎn)單,如下圖缨伊,給一個(gè)已有字符串"abcd"第二次賦值成"abcedl"摘刑,不是在原內(nèi)存地址上修改數(shù)據(jù),而是重新指向一個(gè)新對(duì)象刻坊,新地址枷恕。
2. String為什么不可變?
翻開(kāi)JDK源碼谭胚,java.lang.String類起手前三行徐块,是這樣寫(xiě)的:
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {/** String本質(zhì)是個(gè)char數(shù)組. 而且用final關(guān)鍵字修飾.*/private final char value[]; ... ...}
首先String類是用final關(guān)鍵字修飾,這說(shuō)明String不可繼承灾而。再看下面胡控,String類的主力成員字段value是個(gè)char[ ]數(shù)組,而且是用final修飾的绰疤。final修飾的字段創(chuàng)建以后就不可改變铜犬。 有的人以為故事就這樣完了,其實(shí)沒(méi)有轻庆。因?yàn)殡m然value是不可變癣猾,也只是value這個(gè)引用地址不可變。擋不住Array數(shù)組是可變的事實(shí)余爆。Array的數(shù)據(jù)結(jié)構(gòu)看下圖
也就是說(shuō)Array變量只是stack上的一個(gè)引用纷宇,數(shù)組的本體結(jié)構(gòu)在heap堆。String類里的value用final修飾蛾方,只是說(shuō)stack里的這個(gè)叫value的引用地址不可變像捶。沒(méi)有說(shuō)堆里array本身數(shù)據(jù)不可變上陕。看下面這個(gè)例子拓春,
final int[] value={1,2,3}int[] another={4,5,6};value=another; //編譯器報(bào)錯(cuò)释簿,final不可變
value用final修飾,編譯器不允許我把value指向堆區(qū)另一個(gè)地址硼莽。但如果我直接對(duì)數(shù)組元素動(dòng)手庶溶,分分鐘搞定。
final int[] value={1,2,3};value[2]=100; //這時(shí)候數(shù)組里已經(jīng)是{1,2,100}
或者更粗暴的反射直接改懂鸵,也是可以的偏螺。
final int[] array={1,2,3};Array.set(array,2,100); //數(shù)組也被改成{1,2,100}
所以String是不可變,關(guān)鍵是因?yàn)镾UN公司的工程師匆光,在后面所有String的方法里很小心的沒(méi)有去動(dòng)Array里的元素套像,沒(méi)有暴露內(nèi)部成員字段。
private final char value[]這一句里终息,private的私有訪問(wèn)權(quán)限的作用都比f(wàn)inal大夺巩。而且設(shè)計(jì)師還很小心地把整個(gè)String設(shè)成final禁止繼承,避免被其他人繼承后破壞采幌。所以String是不可變的關(guān)鍵都在底層的實(shí)現(xiàn)劲够,而不是一個(gè)final⌒莅考驗(yàn)的是工程師構(gòu)造數(shù)據(jù)類型征绎,封裝數(shù)據(jù)的功力。
3. 不可變有什么好處磨取?
這個(gè)最簡(jiǎn)單的原因人柿,就是為了安全。
示例1
package _12_01字符串;public class 為什么String要設(shè)計(jì)成不可變類你 { public static void main(String[] args) { String a, b, c; a = "test"; b = a; c = b; String processA = processA(a); String processB = processB(b); String processC = processC(c); System.out.println(processA); System.out.println(processB); System.out.println(processC); } static String processA(String str){ return str + "A"; } static String processB(String str){ return str + "B"; } static String processC(String str){ return str + "C"; }}//OUTPUT// testA//testB//testC
當(dāng)String支持非可變性的時(shí)候忙厌,它們的值很好確定凫岖,不管調(diào)用哪個(gè)方法,都互不影響逢净。
如果String是可變的哥放,就可能如下例,我們使用StringBuffer來(lái)模擬String是可變的
package _12_01字符串;public class 為什么String要設(shè)計(jì)成不可變類2 { public static void main(String[] args) { StringBuffer a, b, c; a = new StringBuffer("test"); b = a; c = b; String processA = processA(a); String processB = processB(b); String processC = processC(c); System.out.println(processA); System.out.println(processB); System.out.println(processC); } static String processA(StringBuffer str){ return str.append("A").toString(); } static String processB(StringBuffer str){ return str.append("B").toString(); } static String processC(StringBuffer str){ return str.append("C").toString(); }}//OUTPUT// testA//testAB//testABC
能看出b=a,c=b;程序員的本意是希望變量是不變的爹土。所以String不可變的安全性就體現(xiàn)在這里甥雕。實(shí)際上StringBuffer的作用就是起到了String的可變配套類角色。
示例2
再看下面這個(gè)HashSet用StringBuilder做元素的場(chǎng)景胀茵,問(wèn)題就更嚴(yán)重了社露,而且更隱蔽。
class Test{public static void main(String[] args){HashSet<StringBuilder> hs=new HashSet<StringBuilder>();StringBuilder sb1=new StringBuilder("aaa");StringBuilder sb2=new StringBuilder("aaabbb");hs.add(sb1);hs.add(sb2); //這時(shí)候HashSet里是{"aaa","aaabbb"}StringBuilder sb3=sb1;sb3.append("bbb"); //這時(shí)候HashSet里是{"aaabbb","aaabbb"}System.out.println(hs);}}//Output://[aaabbb, aaabbb]
StringBuilder型變量sb1和sb2分別指向了堆內(nèi)的字面量"aaa"和"aaabbb"琼娘。把他們都插入一個(gè)HashSet峭弟。到這一步?jīng)]問(wèn)題附鸽。但如果后面我把變量sb3也指向sb1的地址,再改變sb3的值瞒瘸,因?yàn)镾tringBuilder沒(méi)有不可變性的保護(hù)坷备,sb3直接在原先"aaa"的地址上改。導(dǎo)致sb1的值也變了挨务。這時(shí)候击你,HashSet上就出現(xiàn)了兩個(gè)相等的鍵值"aaabbb"。破壞了HashSet鍵值的唯一性谎柄。所以千萬(wàn)不要用可變類型做HashMap和HashSet鍵值。
不可變性支持線程安全
還有一個(gè)大家都知道惯雳,就是在并發(fā)場(chǎng)景下朝巫,多個(gè)線程同時(shí)讀一個(gè)資源,是不會(huì)引發(fā)竟態(tài)條件的石景。只有對(duì)資源做寫(xiě)操作才有危險(xiǎn)劈猿。不可變對(duì)象不能被寫(xiě),所以線程安全潮孽。
不可變性支持字符串常量池
最后別忘了String另外一個(gè)字符串常量池的屬性揪荣。像下面這樣字符串one和two都用字面量"something"賦值。它們其實(shí)都指向同一個(gè)內(nèi)存地址往史。
String one = "someString";String two = "someString";
這樣在大量使用字符串的情況下仗颈,可以節(jié)省內(nèi)存空間,提高效率椎例。但之所以能實(shí)現(xiàn)這個(gè)特性挨决,String的不可變性是最基本的一個(gè)必要條件。要是內(nèi)存里字符串內(nèi)容能改來(lái)改去订歪,這么做就完全沒(méi)有意義了脖祈。