摘要:為了減少在中創(chuàng)建的字符串的數(shù)量,字符串類維護了一個字符串常量池。但是當(dāng)執(zhí)行了方法后,將指向字符串常量池中的那個字符串常量。由于和都是字符串常量池中的字面量的引用,所以。究其原因,是因為常量池要保存的是已確定的字面量值。
String,是Java中除了基本數(shù)據(jù)類型以外,最為重要的一個類型了。很多人會認(rèn)為他比較簡單。但是和String有關(guān)的面試題有很多,下面我隨便找兩道面試題,看看你能不能都答對:
Q1:String s = new String("hollis");定義了幾個對象。
Q2:如何理解String的intern方法
上面這兩個是面試題和String相關(guān)的比較??嫉模芏嗳艘话愣贾来鸢?。
A1:若常量池中已經(jīng)存在"hollis",則直接引用,也就是此時只會創(chuàng)建一個對象,如果常量池中不存在"hollis",則先創(chuàng)建后引用,也就是有兩個。
A2:當(dāng)一個String實例str調(diào)用intern()方法時,Java查找常量池中是否有相同Unicode的字符串常量,如果有,則返回其的引用,如果沒有,則在常量池中增加一個Unicode等于str的字符串并返回它的引用;
兩個答案看上去沒有任何問題,但是,仔細想想好像哪里不對呀。按照上面的兩個面試題的回答,就是說new String也會檢查常量池,如果有的話就直接引用,如果不存在就要在常量池創(chuàng)建一個,那么還要intern干啥?難道以下代碼是沒有意義的嗎?
String s = new String("Hollis").intern();
如果,每當(dāng)我們使用new創(chuàng)建字符串的時候,都會到字符串池檢查,然后返回。那么以下代碼也應(yīng)該輸出結(jié)果都是true?
String s1 = "Hollis"; String s2 = new String("Hollis"); String s3 = new String("Hollis").intern(); System.out.println(s1 == s2); System.out.println(s1 == s3);
但是,以上代碼輸出結(jié)果為(base jdk1.8.0_73):
false true
不知道,聰明的讀者看完這段代碼之后,是不是有點被搞蒙了,到底是怎么回事兒?
別急,且聽我慢慢道來。
字面量和運行時常量池JVM為了提高性能和減少內(nèi)存開銷,在實例化字符串常量的時候進行了一些優(yōu)化。為了減少在JVM中創(chuàng)建的字符串的數(shù)量,字符串類維護了一個字符串常量池。
在JVM運行時區(qū)域的方法區(qū)中,有一塊區(qū)域是運行時常量池,主要用來存儲編譯期生成的各種字面量和符號引用。
了解Class文件結(jié)構(gòu)或者做過Java代碼的反編譯的朋友可能都知道,在java代碼被javac編譯之后,文件結(jié)構(gòu)中是包含一部分Constant pool的。比如以下代碼:
public static void main(String[] args) { String s = "Hollis"; }
經(jīng)過編譯后,常量池內(nèi)容如下:
Constant pool: #1 = Methodref #4.#20 // java/lang/Object."":()V #2 = String #21 // Hollis #3 = Class #22 // StringDemo #4 = Class #23 // java/lang/Object ... #16 = Utf8 s .. #21 = Utf8 Hollis #22 = Utf8 StringDemo #23 = Utf8 java/lang/Object
上面的Class文件中的常量池中,比較重要的幾個內(nèi)容:
#16 = Utf8 s #21 = Utf8 Hollis #22 = Utf8 StringDemo
上面幾個常量中,s就是前面提到的符號引用,而Hollis就是前面提到的字面量。而Class文件中的常量池部分的內(nèi)容,會在運行期被運行時常量池加載進去。關(guān)于字面量,詳情參考Java SE Specifications
new String創(chuàng)建了幾個對象下面,我們可以來分析下String s = new String("Hollis");創(chuàng)建對象情況了。
這段代碼中,我們可以知道的是,在編譯期,符號引用s和字面量Hollis會被加入到Class文件的常量池中,然后在類加載階段(具體時間段參考Java 中new String("字面量") 中 "字面量" 是何時進入字符串常量池的?),這兩個常量會進入常量池。
但是,這個“進入”階段,并不會直接把所有類中定義的常量全部都加載進來,而是會做個比較,如果需要加到字符串常量池中的字符串已經(jīng)存在,那么就不需要再把字符串字面量加載進來了。
所以,當(dāng)我們說<若常量池中已經(jīng)存在"hollis",則直接引用,也就是此時只會創(chuàng)建一個對象>說的就是這個字符串字面量在字符串池中被創(chuàng)建的過程。
說完了編譯期的事兒了,該到運行期了,在運行期,new String("Hollis");執(zhí)行到的時候,是要在Java堆中創(chuàng)建一個字符串對象的,而這個對象所對應(yīng)的字符串字面量是保存在字符串常量池中的。但是,String s = new String("Hollis");,對象的符號引用s是保存在Java虛擬機棧上的,他保存的是堆中剛剛創(chuàng)建出來的的字符串對象的引用。
所以,你也就知道以下代碼輸出結(jié)果為false的原因了。
String s1 = new String("Hollis"); String s2 = new String("Hollis"); System.out.println(s1 == s2);
因為,==比較的是s1和s2在堆中創(chuàng)建的對象的地址,當(dāng)然不同了。但是如果使用equals,那么比較的就是字面量的內(nèi)容了,那就會得到true。
在不同版本的JDK中,Java堆和字符串常量池之間的關(guān)系也是不同的,這里為了方便表述,就畫成兩個獨立的物理區(qū)域了。具體情況請參考Java虛擬機規(guī)范。
所以,String s = new String("Hollis");創(chuàng)建幾個對象的答案你也就清楚了。
常量池中的“對象”是在編譯期就確定好了的,在類被加載的時候創(chuàng)建的,如果類加載時,該字符串常量在常量池中已經(jīng)有了,那這一步就省略了。堆中的對象是在運行期才確定的,在代碼執(zhí)行到new的時候創(chuàng)建的。
運行時常量池的動態(tài)擴展編譯期生成的各種字面量和符號引用是運行時常量池中比較重要的一部分來源,但是并不是全部。那么還有一種情況,可以在運行期像運行時常量池中增加常量。那就是String的intern方法。
當(dāng)一個String實例調(diào)用intern()方法時,Java查找常量池中是否有相同Unicode的字符串常量,如果有,則返回其的引用,如果沒有,則在常量池中增加一個Unicode等于str的字符串并返回它的引用;
intern()有兩個作用,第一個是將字符串字面量放入常量池(如果池沒有的話),第二個是返回這個常量的引用。
我們再來看下開頭的那個讓人產(chǎn)生疑惑的例子:
String s1 = "Hollis"; String s2 = new String("Hollis"); String s3 = new String("Hollis").intern(); System.out.println(s1 == s2); System.out.println(s1 == s3);
你可以簡單的理解為String s1 = "Hollis";和String s3 = new String("Hollis").intern();做的事情是一樣的(但實際有些區(qū)別,這里暫不展開)。都是定義一個字符串對象,然后將其字符串字面量保存在常量池中,并把這個字面量的引用返回給定義好的對象引用。
對于String s3 = new String("Hollis").intern();,在未調(diào)用intern時候,s3指向的是JVM在堆中創(chuàng)建的那個對象的引用的(如圖中的s2)。但是當(dāng)執(zhí)行了intern方法后,s3將指向字符串常量池中的那個字符串常量。
由于s1和s3都是字符串常量池中的字面量的引用,所以s1==s3。但是,s2的引用是堆中的對象,所以s2!=s1。
intern的正確用法不知道,你有沒有發(fā)現(xiàn),在String s3 = new String("Hollis").intern();中,其實intern是多余的?
因為就算不用intern,Hollis作為一個字面量也會被加載到Class文件的常量池,進而加入到運行時常量池中,為啥還要多此一舉呢?到底什么場景下才需要使用intern呢?
在解釋這個之前,我們先來看下以下代碼:
String s1 = "Hollis"; String s2 = "Chuang"; String s3 = s1 + s2; String s4 = "Hollis" + "Chuang";
在經(jīng)過反編譯后,得到代碼如下:
String s1 = "Hollis"; String s2 = "Chuang"; String s3 = (new StringBuilder()).append(s1).append(s2).toString(); String s4 = "HollisChuang";
可以發(fā)現(xiàn),同樣是字符串拼接,s3和s4在經(jīng)過編譯器編譯后的實現(xiàn)方式并不一樣。s3被轉(zhuǎn)化成StringBuilder及append,而s4被直接拼接成新的字符串。
如果你感興趣,你還能發(fā)現(xiàn),String s3 = s1 + s2; 經(jīng)過編譯之后,常量池中是有兩個字符串常量的分別是 Hollis、Chuang(其實Hollis和Chuang是String s1 = "Hollis";和String s2 = "Chuang";定義出來的),拼接結(jié)果HollisChuang并不在常量池中。
如果代碼只有String s4 = "Hollis" + "Chuang";,那么常量池中將只有HollisChuang而沒有"Hollis" 和 "Chuang"。
究其原因,是因為常量池要保存的是已確定的字面量值。也就是說,對于字符串的拼接,純字面量和字面量的拼接,會把拼接結(jié)果作為常量保存到字符串。
如果在字符串拼接中,有一個參數(shù)是非字面量,而是一個變量的話,整個拼接操作會被編譯成StringBuilder.append,這種情況編譯器是無法知道其確定值的。只有在運行期才能確定。
那么,有了這個特性了,intern就有用武之地了。那就是很多時候,我們在程序中得到的字符串是只有在運行期才能確定的,在編譯期是無法確定的,那么也就沒辦法在編譯期被加入到常量池中。
這時候,對于那種可能經(jīng)常使用的字符串,使用intern進行定義,每次JVM運行到這段代碼的時候,就會直接把常量池中該字面值的引用返回,這樣就可以減少大量字符串對象的創(chuàng)建了。
如一深入解析String#intern文中舉的一個例子:
static final int MAX = 1000 * 10000; static final String[] arr = new String[MAX]; public static void main(String[] args) throws Exception { Integer[] DB_DATA = new Integer[10]; Random random = new Random(10 * 10000); for (int i = 0; i < DB_DATA.length; i++) { DB_DATA[i] = random.nextInt(); } long t = System.currentTimeMillis(); for (int i = 0; i < MAX; i++) { arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length])).intern(); } System.out.println((System.currentTimeMillis() - t) + "ms"); System.gc(); }
在以上代碼中,我們明確的知道,會有很多重復(fù)的相同的字符串產(chǎn)生,但是這些字符串的值都是只有在運行期才能確定的。所以,只能我們通過intern顯示的將其加入常量池,這樣可以減少很多字符串的重復(fù)創(chuàng)建。
總結(jié)我們再回到文章開頭那個疑惑:按照上面的兩個面試題的回答,就是說new String也會檢查常量池,如果有的話就直接引用,如果不存在就要在常量池創(chuàng)建一個,那么還要intern干啥?難道以下代碼是沒有意義的嗎?
String s = new String("Hollis").intern();
而intern中說的“如果有的話就直接返回其引用”,指的是會把字面量對象的引用直接返回給定義的對象。這個過程是不會在Java堆中再創(chuàng)建一個String對象的。
的確,以上代碼的寫法其實是使用intern是沒什么意義的。因為字面量Hollis會作為編譯期常量被加載到運行時常量池。
之所以能有以上的疑惑,其實是對字符串常量池、字面量等概念沒有真正理解導(dǎo)致的。有些問題其實就是這樣,單個問題,自己都知道答案,但是多個問題綜合到一起就蒙了。歸根結(jié)底是知識的理解還停留在點上,沒有串成面。
本文中的內(nèi)容歡迎大家討論,如有偏頗歡迎指正,文中例子是為了方面講解特意舉的,如有不當(dāng)之處望諒解。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/71397.html
摘要:使用可以方便的對字符串進行拼接。該方法使用進行聲明,說明是一個線程安全的方法。所以,阿里巴巴開發(fā)手冊建議循環(huán)體內(nèi),字符串的連接方式,使用的方法進行擴展。但是,還要強調(diào)的是如果不是在循環(huán)體中進行字符串拼接的話,直接使用就好了。 摘要: 學(xué)習(xí)阿里巴巴Java開發(fā)手冊。 原文:為什么阿里巴巴不建議在for循環(huán)中使用+進行字符串拼接 微信公眾號:Hollis Fundebug經(jīng)授權(quán)轉(zhuǎn)載,...
摘要:一結(jié)構(gòu)體的聲明與定義結(jié)構(gòu)體的聲明結(jié)構(gòu)是一些值的集合,這些值稱為成員變量。但是結(jié)構(gòu)體變量的變量名并不是指向該結(jié)構(gòu)體的地址,所以要使用取地址運算符才能獲取其地址。因此,結(jié)構(gòu)體傳參的時候,要傳結(jié)構(gòu)體的地址。 ...
摘要:摘要本文主要介紹了亞馬遜的使用過程中發(fā)現(xiàn)的問題以及基于亞馬遜實例自己搭建服務(wù)器的一些經(jīng)驗。之前公司使用亞馬遜的實例,一切都非常好。但是我們架設(shè)在亞馬遜實例上的服務(wù)器為了安全起見都是跨網(wǎng)段的,不支持,實現(xiàn)不了啊。 摘要 本文主要介紹了亞馬遜RDS的使用過程中發(fā)現(xiàn)的問題以及基于亞馬遜EC2實例自己搭建Mysql服務(wù)器的一些經(jīng)驗。 showImg(https://segmentfault.c...
摘要:同理,若為,返回的結(jié)果若為或者,且為,返回的結(jié)果。同理,若為或者,且為,返回的結(jié)果是對象轉(zhuǎn)換基本類型的方法??磦€例子根據(jù)上述規(guī)則來解析為,上式為第條上式為第條上式為,上式為第條上式為 前不久因為一個項目設(shè)計的問題,煩心了好幾天,為了不留坑擁抱強類型語言特點,還是選擇了===作為數(shù)據(jù)判斷是否相等,對于==今天來探究一下貓膩(弱類型的JavaScript的坑真的太多了,typescript...
閱讀 2425·2021-11-18 10:02
閱讀 1938·2021-10-13 09:40
閱讀 3017·2021-09-07 10:07
閱讀 2125·2021-09-04 16:48
閱讀 1021·2019-08-30 13:18
閱讀 2467·2019-08-29 14:03
閱讀 2936·2019-08-29 12:54
閱讀 3172·2019-08-26 11:41