Java中有關string
String,是Java中除了基本資料型別以外,最為重要的一個型別了。很多人會認為他比較簡單。但是和String有關的面試題有很多,下面我隨便找兩道面試題,看看你能不能都答對:
Q1:String s = new String("hollis");
定義了幾個物件。
Q2:如何理解String
的intern
方法?
上面這兩個是面試題和String相關的比較常考的,很多人一般都知道答案。
A1:若常量池中已經存在"hollis",則直接引用,也就是此時只會建立一個物件,如果常量池中不存在"hollis",則先建立後引用,也就是有兩個。
A2:當一個String例項呼叫intern()方法時,JVM會查詢常量池中是否有相同Unicode的字串常量,如果有,則返回其的引用,如果沒有,則在常量池中增加一個Unicode等於str的字串並返回它的引用;
兩個答案看上去沒有任何問題,但是,仔細想想好像哪裡不對呀。
按照上面的兩個面試題的回答,就是說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):
false
true
不知道,聰明的讀者看完這段程式碼之後,是不是有點被搞蒙了,到底是怎麼回事兒?
別急,且聽我慢慢道來。
字面量和執行時常量池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檔案的常量池中,然後在類載入階段,這兩個常量會進入常量池。
但是,這個“進入”過程,並不會直接把所有類中定義的常量全部都載入進來,而是會做個比較,如果需要加到字串常量池中的字串已經存在,那麼就不需要再把字串字面量載入進來了。
所以,當我們說<若常量池中已經存在"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
。
在不同版本的JDK中,Java堆和字串常量池之間的關係也是不同的,這裡為了方便表述,就畫成兩個獨立的物理區域了。具體情況請參考Java虛擬機器規範。
上圖中s1和s2是兩個完全不同的物件,在堆中有自己的記憶體空間,當然不相等了。所以,
String s = new String("Hollis");
建立幾個物件的答案你也就清楚了。常量池中的“物件”是在編譯期就確定好了的,在類被載入的時候建立的,如果類載入時,該字串常量在常量池中已經有了,那這一步就省略了。堆中的物件是在執行期才確定的,在程式碼執行到new的時候建立的。執行時常量池的動態擴充套件
編譯期生成的各種字面量和符號引用是執行時常量池中比較重要的一部分來源,但是並不是全部。那麼還有一種情況,可以在執行期像執行時常量池中增加常量。那就是String
的intern
方法。
當一個String
例項呼叫intern()
方法時,JVM會查詢常量池中是否有相同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();
做的事情是一樣的(但實際有些區別,這裡暫不展開)。都是定義一個字串物件,然後將其字串字面量儲存在常量池中,並把這個字面量的引用返回給定義好的物件引用。如下圖:
對於String s3 = new String("Hollis").intern();
,在呼叫intern
之前,s3指向的是JVM在堆中建立的那個物件的引用的(如圖中的s2)。但是當執行了intern
方法後,s3將指向字串常量池中的那個字串常量。
由於s1和s3都是字串常量池中的字面量的引用,所以s1==s3。但是,s2的引用是堆中的物件,所以s2!=s1。
intern的正確用法不知道,你有沒有發現,在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 s4 = 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();
}
for (int i = 0; i < MAX; i++) {
arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length])).intern();
}
}
在以上程式碼中,我們明確的知道,會有很多重複的相同的字串產生,但是這些字串的值都是隻有在執行期才能確定的。所以,只能我們通過intern
顯示的將其加入常量池,這樣可以減少很多字串的重複建立。
我們再回到文章開頭那個疑惑:按照上面的兩個面試題的回答,就是說new String
也會檢查常量池,如果有的話就直接引用,如果不存在就要在常量池建立一個,那麼還要intern
幹啥?難道以下程式碼是沒有意義的嗎?
String s = new String("Hollis").intern();
new String 所謂的“如果有的話就直接引用”,指的是Java堆中建立的String物件中包含的字串字面量直接引用字串池中的字面量物件。也就是說,還是要在堆裡面建立物件的。
而intern中說的“如果有的話就直接返回其引用”,指的是會把字面量物件的引用直接返回給定義的物件。這個過程是不會在Java堆中再建立一個String物件的。
的確,以上程式碼的寫法其實是使用intern
是沒什麼意義的。因為字面量Hollis會作為編譯期常量被載入到執行時常量池。
之所以能有以上的疑惑,其實是對字串常量池、字面量等概念沒有真正理解導致的。有些問題其實就是這樣,單個問題,自己都知道答案,但是多個問題綜合到一起就蒙了。歸根結底是知識的理解還停留在點上,沒有串成面。
相關文章
- Java String的相關性質分析Java
- 面試之Java String 編碼相關面試Java
- 記Java中有關記憶體的簡單認識Java記憶體
- Java String類Java
- java反射中有哪些APIJava反射API
- java中有哪些基本註解Java
- Java使用類-StringJava
- Java String.intern()Java
- Java String length()例子Java
- Java String常量池Java
- Java-string字串Java字串
- Java String 詳解Java
- JavaScript中有關new的問題JavaScript
- 《Java工程師成神之路-基礎篇》Java基礎知識——String相關Java工程師
- 【Java】String、StringBuilder和StringBufferJavaUI
- Java新人之路 -- String類Java
- java String類說明Java
- Java String原始碼分析Java原始碼
- 聊聊java String的internJava
- 深入研究Java StringJava
- 33.Java-String分析Java
- Java進階01 String類Java
- java 常用類-String-1Java
- java String類練習題Java
- 真的懂Java的String嗎?Java
- JAVA中String format的用法JavaORM
- Java-- String原始碼分析Java原始碼
- Java 20中有哪些新功能? - symflowerJava
- volatile關鍵字在併發中有哪些作用?
- Java--String類查詢方法Java
- Java String 物件,你瞭解多少?Java物件
- Java String和Date的轉換Java
- java.lang.NumberFormatException: For input string: “M“JavaORMException
- java.sql.SQLException: Incorrect string valueJavaSQLException
- Java雜記17—String全面解析Java
- Java中String類的常用方法Java
- java複習之 String,StringBuffer,StringBuilderJavaUI
- 【JAVA】筆記(8)--- java.lang.String 精講Java筆記