String,是Java中除了基本數據類型之外,最爲重要的一個類型了。不少人會認爲他比較簡單。可是和String有關的面試題有不少,下面我隨便找兩道面試題,看看你能不能都答對:html
Q1:String s = new String("hollis");
定義了幾個對象。java
Q2:如何理解String
的intern
方法面試
上面這兩個是面試題和String相關的比較常考的,不少人通常都知道答案。oracle
A1:若常量池中已經存在"hollis",則直接引用,也就是此時只會建立一個對象,若是常量池中不存在"hollis",則先建立後引用,也就是有兩個。app
A2:當一個String實例str調用intern()方法時,Java查找常量池中是否有相同Unicode的字符串常量,若是有,則返回其的引用,若是沒有,則在常量池中增長一個Unicode等於str的字符串並返回它的引用;dom
兩個答案看上去沒有任何問題,可是,仔細想一想好像哪裏不對呀。按照上面的兩個面試題的回答,就是說new String
也會檢查常量池,若是有的話就直接引用,若是不存在就要在常量池建立一個,那麼還要intern幹啥?難道如下代碼是沒有意義的嗎?性能
String s = new String("Hollis").intern();
若是,每當咱們使用new建立字符串的時候,都會到字符串池檢查,而後返回。那麼如下代碼也應該輸出結果都是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);
可是,以上代碼輸出結果爲(base jdk1.8.0_73):ui
false true
不知道,聰明的讀者看完這段代碼以後,是否是有點被搞蒙了,究竟是怎麼回事兒?spa
別急,且聽我慢慢道來。
JVM爲了提升性能和減小內存開銷,在實例化字符串常量的時候進行了一些優化。爲了減小在JVM中建立的字符串的數量,字符串類維護了一個字符串常量池。
在JVM運行時區域的方法區中,有一塊區域是運行時常量池,主要用來存儲編譯期生成的各類字面量和符號引用。
瞭解Class文件結構或者作過Java代碼的反編譯的朋友可能都知道,在java代碼被javac
編譯以後,文件結構中是包含一部分Constant pool
的。好比如下代碼:
public static void main(String[] args) { String s = "Hollis"; }
通過編譯後,常量池內容以下:
Constant pool: #1 = Methodref #4.#20 // java/lang/Object."<init>":()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文件中的常量池中,比較重要的幾個內容:
#16 = Utf8 s #21 = Utf8 Hollis #22 = Utf8 StringDemo
上面幾個常量中,s
就是前面提到的符號引用,而Hollis
就是前面提到的字面量。而Class文件中的常量池部分的內容,會在運行期被運行時常量池加載進去。關於字面量,詳情參考Java SE Specifications
下面,咱們能夠來分析下String s = new String("Hollis");
建立對象狀況了。
這段代碼中,咱們能夠知道的是,在編譯期,符號引用s
和字面量Hollis
會被加入到Class文件的常量池中,而後在類加載階段(具體時間段參考Java 中new String("字面量") 中 "字面量" 是什麼時候進入字符串常量池的?),這兩個常量會進入常量池。
可是,這個「進入」階段,並不會直接把全部類中定義的常量所有都加載進來,而是會作個比較,若是須要加到字符串常量池中的字符串已經存在,那麼就不須要再把字符串字面量加載進來了。
因此,當咱們說<若常量池中已經存在"hollis",則直接引用,也就是此時只會建立一個對象>說的就是這個字符串字面量在字符串池中被建立的過程。
說完了編譯期的事兒了,該到運行期了,在運行期,new String("Hollis");
執行到的時候,是要在Java堆中建立一個字符串對象的,而這個對象所對應的字符串字面量是保存在字符串常量池中的。可是,String s = new String("Hollis");
,對象的符號引用s
是保存在Java虛擬機棧上的,他保存的是堆中剛剛建立出來的的字符串對象的引用。
因此,你也就知道如下代碼輸出結果爲false的緣由了。
String s1 = new String("Hollis"); String s2 = new String("Hollis"); System.out.println(s1 == s2);
由於,==
比較的是s1
和s2
在堆中建立的對象的地址,固然不一樣了。可是若是使用equals
,那麼比較的就是字面量的內容了,那就會獲得true
。
<img src="https://user-gold-cdn.xitu.io...;h=337&f=png&s=57202" alt="string" width="897" height="337" class="aligncenter size-full wp-image-2540" />
在不一樣版本的JDK中,Java堆和字符串常量池之間的關係也是不一樣的,這裏爲了方便表述,就畫成兩個獨立的物理區域了。具體狀況請參考Java虛擬機規範。
因此,String s = new String("Hollis");
建立幾個對象的答案你也就清楚了。
常量池中的「對象」是在編譯期就肯定好了的,在類被加載的時候建立的,若是類加載時,該字符串常量在常量池中已經有了,那這一步就省略了。堆中的對象是在運行期才肯定的,在代碼執行到new的時候建立的。
編譯期生成的各類字面量和符號引用是運行時常量池中比較重要的一部分來源,可是並非所有。那麼還有一種狀況,能夠在運行期像運行時常量池中增長常量。那就是String
的intern
方法。
當一個String
實例調用intern()
方法時,Java查找常量池中是否有相同Unicode的字符串常量,若是有,則返回其的引用,若是沒有,則在常量池中增長一個Unicode等於str的字符串並返回它的引用;
intern()有兩個做用,第一個是將字符串字面量放入常量池(若是池沒有的話),第二個是返回這個常量的引用。
咱們再來看下開頭的那個讓人產生疑惑的例子:
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();
作的事情是同樣的(但實際有些區別,這裏暫不展開)。都是定義一個字符串對象,而後將其字符串字面量保存在常量池中,並把這個字面量的引用返回給定義好的對象引用。
<img src="https://user-gold-cdn.xitu.io...;h=460&f=png&s=79145" alt="intern" width="1024" height="460" class="aligncenter size-full wp-image-2541" />
對於String s3 = new String("Hollis").intern();
,在未調用intern
時候,s3指向的是JVM在堆中建立的那個對象的引用的(如圖中的s2)。可是當執行了intern
方法後,s3將指向字符串常量池中的那個字符串常量。
因爲s1和s3都是字符串常量池中的字面量的引用,因此s1==s3。可是,s2的引用是堆中的對象,因此s2!=s1。
不知道,你有沒有發現,在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";
在通過反編譯後,獲得代碼以下:
String s1 = "Hollis"; String s2 = "Chuang"; String s3 = (new StringBuilder()).append(s1).append(s2).toString(); String s4 = "HollisChuang";
能夠發現,一樣是字符串拼接,s3和s4在通過編譯器編譯後的實現方式並不同。s3被轉化成StringBuilder
及append
,而s4被直接拼接成新的字符串。
若是你感興趣,你還能發現,String s3 = s1 + s2;
通過編譯以後,常量池中是有兩個字符串常量的分別是 Hollis
、Chuang
(其實Hollis
和Chuang
是String s1 = "Hollis";
和String s2 = "Chuang";
定義出來的),拼接結果HollisChuang
並不在常量池中。
若是代碼只有String s4 = "Hollis" + "Chuang";
,那麼常量池中將只有HollisChuang
而沒有"Hollis" 和 "Chuang"。
究其緣由,是由於常量池要保存的是已肯定的字面量值。也就是說,對於字符串的拼接,純字面量和字面量的拼接,會把拼接結果做爲常量保存到字符串。
若是在字符串拼接中,有一個參數是非字面量,而是一個變量的話,整個拼接操做會被編譯成StringBuilder.append
,這種狀況編譯器是沒法知道其肯定值的。只有在運行期才能肯定。
那麼,有了這個特性了,intern
就有用武之地了。那就是不少時候,咱們在程序中獲得的字符串是隻有在運行期才能肯定的,在編譯期是沒法肯定的,那麼也就沒辦法在編譯期被加入到常量池中。
這時候,對於那種可能常用的字符串,使用intern
進行定義,每次JVM運行到這段代碼的時候,就會直接把常量池中該字面值的引用返回,這樣就能夠減小大量字符串對象的建立了。
如一深刻解析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(); }
在以上代碼中,咱們明確的知道,會有不少重複的相同的字符串產生,可是這些字符串的值都是隻有在運行期才能肯定的。因此,只能咱們經過intern
顯示的將其加入常量池,這樣能夠減小不少字符串的重複建立。
咱們再回到文章開頭那個疑惑:按照上面的兩個面試題的回答,就是說new String
也會檢查常量池,若是有的話就直接引用,若是不存在就要在常量池建立一個,那麼還要intern
幹啥?難道如下代碼是沒有意義的嗎?
String s = new String("Hollis").intern();
而intern中說的「若是有的話就直接返回其引用」,指的是會把字面量對象的引用直接返回給定義的對象。這個過程是不會在Java堆中再建立一個String對象的。
的確,以上代碼的寫法實際上是使用intern是沒什麼意義的。由於字面量Hollis會做爲編譯期常量被加載到運行時常量池。
之因此能有以上的疑惑,實際上是對字符串常量池、字面量等概念沒有真正理解致使的。有些問題其實就是這樣,單個問題,本身都知道答案,可是多個問題綜合到一塊兒就蒙了。歸根結底是知識的理解還停留在點上,沒有串成面。
本文中的內容歡迎你們討論,若有偏頗歡迎指正,文中例子是爲了方面講解特地舉的,若有不當之處望諒解。