字符串是不可變的
究竟什么使字符串字面量這么特殊?首先,記住重要的一點(diǎn)是字符串對(duì)象是不可變的。
這就意味著一旦創(chuàng)建,一個(gè)字符串對(duì)象就不能被改變(還是可以通過反射來改變)。
不可變?不能被更改?那怎么解釋這段代碼。
public?class?ImmutableStrings{????public?static?void?main(String[]?args)????{????????String?start?=?"Hello";????????String?end?=?start.concat("?World!");????????System.out.println(end);????}}//?OutputHello?World!
看這段代碼,字符串被改變了嗎,還是沒有?事實(shí)上,這段代碼中并沒有字符串對(duì)象被改變。
我們首先將“Hello”賦值給start變量,為了實(shí)現(xiàn)這步,需要在堆中創(chuàng)建一個(gè)對(duì)象,并把它的引用存儲(chǔ)在start中。接下來,我們?cè)谶@個(gè)對(duì)象上調(diào)用concat(String)方法。進(jìn)行到這里Java耍了一個(gè)小把戲,如果我們查看String的API說明,會(huì)發(fā)現(xiàn)其中對(duì)于concat(String)方法有如下的描述:
方法描述:將指定字符串連接在這個(gè)字符串的結(jié)尾。
如果長(zhǎng)度為0,則返回這個(gè)字符串對(duì)象。否則就創(chuàng)建一個(gè)新的字符串對(duì)象,表示這個(gè)字符串序列由原字符串對(duì)象和參數(shù)字符串二者所表示的字符串序列拼接而成。
你肯定看到了,當(dāng)你將兩個(gè)字符串做拼接操作時(shí),實(shí)際上并沒有改變?cè)瓕?duì)象,而是直接創(chuàng)建了一個(gè)包含原始對(duì)象的新的對(duì)象,并且將另一個(gè)字符串拼在了后面。
我們上面那段代碼就是這么執(zhí)行的,start變量所引用的字符串對(duì)象并沒有改變,如果在調(diào)用concat方法之后System.out.println(start);,會(huì)發(fā)現(xiàn)start仍然指向的是“Hello”。
這時(shí)候你可能想到了字符串中的“+”操作符,事實(shí)上字符串的“+”操作也是和concat做了同樣的事情(“+”操作實(shí)際上是new了一個(gè)StringBuilder對(duì)象,然后調(diào)用append方法)。
字符串的存儲(chǔ)——字符串常量池
你或許聽說過“字符串常量池”這個(gè)概念,究竟什么是字符串常量池?有人說是一個(gè)字符串對(duì)象容器。答案很接近了,但是不完全正確。
事實(shí)上他是一用來保存字符串對(duì)象引用的容器。
即使字符串是不可變的,它仍然和Java中的其他對(duì)象一樣。對(duì)象都是創(chuàng)建在堆中,字符串也不例外。
所以字符串常量池仍然依靠堆,他們存儲(chǔ)的只是堆中字符串的引用。
目前還沒有解釋這個(gè)池到底是什么,或者它為何存在。
好吧,因?yàn)樽址畬?duì)象是不可變的,所以復(fù)制多個(gè)引用來“共享”這個(gè)字符串是安全的。下面來看一個(gè)例子:
public?class?ImmutableStrings{????public?static?void?main(String[]?args)????{????????String?one?=?"someString";????????String?two?=?"someString";????????????????System.out.println(one.equals(two));????????System.out.println(one?==?two);????}}//?Outputtruetrue
在這個(gè)例子中,實(shí)在沒有必要為一個(gè)相同的字符串對(duì)象創(chuàng)建兩個(gè)實(shí)例。如果字符串像StringBuffer一樣是可變的,那么我們會(huì)被迫創(chuàng)建兩個(gè)對(duì)象(如果不這樣做的話,通過一個(gè)引用改變它的值,將會(huì)導(dǎo)致其他引用的值也同樣改變,從而可能發(fā)生錯(cuò)誤)。
但是,我們知道字符串對(duì)象是不能被改變的,我們可以安全地通過兩個(gè)引用one和two來使用一個(gè)字符串對(duì)象。
這個(gè)工作是通過字符串常量池完成的,下面來看一下它是如何完成的:
當(dāng)一個(gè).java文件被編譯成.class文件時(shí),和所有其他常量一樣,每個(gè)字符串字面量都通過一種特殊的方式被記錄下來。
當(dāng)一個(gè).class文件被加載時(shí)(注意加載發(fā)生在初始化之前),JVM在.class文件中尋找字符串字面量。
當(dāng)找到一個(gè)時(shí),JVM會(huì)檢查是否有相等的字符串在常量池中存放了堆中引用。
如果找不到,就會(huì)在堆中創(chuàng)建一個(gè)對(duì)象,然后將它的引用存放在池中的一個(gè)常量表中。
一旦一個(gè)字符串對(duì)象的引用在常量池中被創(chuàng)建,這個(gè)字符串在程序中的所有字面量引用都會(huì)被常量池中已經(jīng)存在的那個(gè)引用代替。
所以,在上面的例子中字符串常量池中只有一個(gè)引用,就是“someString”這個(gè)字符串對(duì)象的引用。
局部變量one和two都被賦予了同一個(gè)字符串對(duì)象的引用??梢酝ㄟ^程序的輸出來驗(yàn)證。
equals方法檢查的是兩個(gè)字符串對(duì)象是否包含相同的數(shù)據(jù)(“someString”),而“==”操作符作用在對(duì)象上比較的是引用是否相同,這意味著只有兩個(gè)引用指向的是同一個(gè)對(duì)象才會(huì)返回true。
所以例子中的兩個(gè)引用是相等的。從輸出可以看到,局部變量one和two不僅包含相同的數(shù)據(jù),而且還指向相同的對(duì)象。
無圖無真相,來看一下他們之間的關(guān)系:
注意,對(duì)于字符串字面量有一點(diǎn)比較特殊。通過“new”關(guān)鍵字構(gòu)建時(shí)一種不同的方式。
下面舉一個(gè)例子:
public?class?ImmutableStrings{????public?static?void?main(String[]?args)????{????????String?one?=?"someString";????????String?two?=?new?String("someString");????????????????System.out.println(one.equals(two));????????System.out.println(one?==?two);????}}//?Outputtruefalse
在這個(gè)例子中,可以看到由于關(guān)鍵字“new”,最后的結(jié)果有一點(diǎn)不同。
此例中,兩個(gè)字符串字面量仍然被放進(jìn)了常量池的常量表中,但是當(dāng)使用“new”時(shí),JVM就會(huì)在運(yùn)行時(shí)創(chuàng)建一個(gè)新對(duì)象,而不是使用常量表中的引用。
雖然例子中的兩個(gè)字符串引用所指向的對(duì)象包含相同的數(shù)據(jù)“someString”,但是這兩個(gè)對(duì)象并不相同。
這一點(diǎn)可以從輸出看出來,equals方法返回了true,而檢查引用是否相等的“==”返回false。
這表明兩個(gè)變量指向的是兩個(gè)不同的字符串對(duì)象。
如果你想看圖形化的表示,下面就是。要記住引用到常量池的字符串對(duì)象是在類加載的時(shí)候創(chuàng)建的,而另一個(gè)對(duì)象是在運(yùn)行時(shí),當(dāng)“new String”語句被執(zhí)行時(shí)。
如果你想得到兩個(gè)引用到相同對(duì)象的局部變量,你可以使用String類中的定義的intern()方法。
調(diào)用two.intern()后,會(huì)在字符串常量池中尋找是否有值相等的對(duì)象引用。
如果有的話,就會(huì)返回這個(gè)引用,然后你可以把它賦給局部變量。
如果這么做的化,局部變量one和two都是同一個(gè)對(duì)象的引用,并且在字符串常量池中也存有一個(gè)引用,就如同第一張圖那樣。這時(shí),在運(yùn)行時(shí)創(chuàng)建的第二個(gè)字符串對(duì)象將會(huì)被GC回收。
以上就是深圳達(dá)內(nèi)教育java培訓(xùn)機(jī)構(gòu)的小編針對(duì)“Java編程技術(shù)分享之字符串字面量”的內(nèi)容進(jìn)行的回答,希望對(duì)大家有所幫助,如有疑問,請(qǐng)?jiān)诰€咨詢,有專業(yè)老師隨時(shí)為你服務(wù)。