本文主要解釋java的intern方法的作用和原理,同時會解釋一下經常問的String面試題。
首先先說一下結論,后面會實際操作,驗證一下結論。intern方法在不同的Java版本中的實現是不一樣的。Java6之前是一種實現,Java6之后也就是Java7和Java8是另外一種實現。
先說一下intern方法的定義 在Java的String類中是這樣定義的,是一個本地方法,其中源碼由C實現
public native String intern();
再來看一下源碼的注釋描述:
* <p>
* When the intern method is invoked, if the pool already contains a
* string equal to this {@code String} object as determined by
* the {@link #equals(Object)} method, then the string from the pool is
* returned. Otherwise, this {@code String} object is added to the
* pool and a reference to this {@code String} object is returned.
* <p>
翻譯過來的意思就是:如果常量池中已經有了此字符串,那么將常量池中該字符串的引用返回,如果沒有,那么將該字符串對象添加到常量池中,並且將引用返回。
首先要明白,這里注釋的該字符串是調用此方法的字符串,返回的是引用。
下面介紹一個new String的知識點,下文再討論intern的實現的會用到。
有個經典的Java基礎面試題,new String("xyz")會創建幾個對象?
動手實踐后,發現再new String("xyz")有可能會創建一個(不好驗證,但是可以通過下文的分析得出結論),也有可能會創建兩個(可驗證)。
結論:如果常量池中沒有 xyz,那么就會創建兩個,現在堆中創建一個,然后將對象copy到常量池中,也就是第二次創建,堆中和常量池中是兩個對象。


上圖的版本是Java8版本。
Java6版本:
intern方法作用:確實如上述注釋上所描述,如果常量池中沒有字符串,則將該字符串對象加入常量池,並返回引用。
** 這里需要注意:Java6中常量池是在方法區中,而Java1.6版本hotspot采用永久帶實現了方法區,永久代是和Java堆區分的,即就是常量池中沒有字符串,那么將該字符串對象放入永久帶的常量池中,並返回其引用。
Java7和Java8版本:
intern方法作用:和注釋描述的並不同,
如果常量池有,那么返回該字符串的引用。
如果常量池沒有,那么如果是”a“.intern調用,那么就會把”a“放入常量池,並返回”a“在常量池中的引用。
如果是new String("a").internal ,其中在 new String的時候上文已經說到過,會在堆和常量池各創建一個對象,那么這里返回的就是常量池的字符串a的引用。
如果是new StringBuilder("a").internal,其中new StringBuilder會在堆中創建一個對象,常量池沒有,這里調用intern方法后,**會將堆中字串a的引用放到常量池,注意這里始終只是創建了一個對象,
返回的引用雖然是常量池的,但是常量池的引用是指向堆中字串a的引用的。
上述粗體字描述部分也就是不同之處,也是intern方法在Java6 之后的使用變化如何體現。
再簡單總結一下Java7和Java8的intern方法作用:如果常量池沒有,那么會將堆中的字符串的引用放到常量池,注意是引用,然后返回該引用。為什么Java7和Java8會不一樣呢,原因就是 Java7之后(部分虛擬機,Hotspot,JRockit)已經將永久代的
常量池、靜態變量移出,放入了Java堆中,而永久代也在Java8中完全廢棄,方法區改名為元空間。既然常量池已經在Java6之后放入了堆中,那么如果堆中已經創建過此字符串的對象了,那么就沒有必要在常量池中再創建一個一毛一樣的對象了,直接將其引用拷貝
返回就好了,因為都是處於同一個區域Java堆中。
下面看幾個實踐:來驗證一下上述的結論:
1、下圖 在new的時候已經創建了兩個對象,第二行,只是獲取的第一行創建的常量池的對象的引用,實際的對象已經創建過了。這里是兩個不同的對象,返回false。

2、和上述一樣,只不過這一次第一行,現在常量池創建了對象,第二行發現常量池已經有了,只在堆上創建了一次對象,但仍然是兩個對象,引用不同,返回false。

3、第一行,Strignbuilder只會在堆中創建一個對象,第二行調用intern方法后,會將堆中的引用放到到常量池中。第三行發現常量池中已經有這個字符串的引用了,直接返回。因此是同一個引用,返回的都是第一次創建的堆中字串的引用

4、和上述3的不同之處在於沒有調用intern方法,因此結果輸出不一樣。

5、new String之后使用 + 在Java中會進行編譯優化,編譯成字節碼指令后,會將+ 優化成 先new Stringbuilder對象,然后調用append方法進行拼接。
如下圖:

反編譯生成字節碼:

因此這里s1最終創建的時候,xyzz字符串並沒有在常量池創建,只是在堆中創建了,因為就如同上面的3一樣,是new Stringbuilder操作。
所以在調用intern操作后,將其堆中的引用放入常量池並返回。所以后面的結果都是true,因為至始至終都是堆中的一個對象。

6、和上述5是相反的,結果輸出也不同。

總結:
在Java6之后,使用intern可以起到優化的作用,但也要看具體的情況,比如在使用加號做字符拼接的時候,如果不想在因為其他的操作在常量池中重新創建相同的對象,那么調用intern方法,在常量池中只會放入一個引用,這時候只創建了一個對象。
