深入理解 new String()
前言
在一些面試中,我們經常會看到一些關於String
的面試題,如String s1 = new String("a") + "b"
這個過程建立了幾個字串物件? 本章就詳細來講述一下不同方式建立String
的過程。
示例一
String s1 = new String("Hello");
首先這裡由於使用new
來建立的物件,肯定會在堆中建立1個字串物件,在Java中,所有的字串字面量會快取在字串常量池
,所以每個字串建立時都會在池內檢視是否存在該字串,如果不存在則會在堆中再新建1個字串物件,然後將它的引用放入字串常量池
。依次推斷,這裡會創建出2個字串物件。
接下來我們通過外掛來看一下這段程式碼的位元組碼執行流程,外掛名稱: jclasslib
0 new #2 <java/lang/String>
3 dup
4 ldc #3 <Hello>
6 invokespecial #4 <java/lang/String.<init> : (Ljava/lang/String;)V>
9 astore_1
10 return
-
0 new #2 <java/lang/String>
表示是通過new
建立了一個String物件,#2
就是經過類載入解析後的直接引用,解析前是java.lang.String
。 -
ldc #3 <Hello>
這段指令會做兩件事 判斷#3
是否已經被解析成直接引用,如果沒有則進行解析。然後檢測字串常量池
Hello
字元常量,如果有則直接獲取引用,反之則在堆中建立一個Hello
字串物件,然後把引用放入字串常量池
。 -
invokespecial #4...
這行指令會執行String的構造方法進行初始化。 -
9 astore_1
將建立好的字串引用掛載到區域性變量表中。
示例二
String s1 = "Hello";
這種方式,沒有通過new
關鍵字,所以它的結果只會是建立0或者1個物件,如果字串常量池
中不存在Hello
常量物件,則進行建立並且放入。如果已經存在,則直接返回引用。
0 ldc #2 <Hello>
2 astore_1
3 return
示例三
String s1 = "He" + "llo";
話不多說,直接看位元組碼
0 ldc #2 <Hello>
2 astore_1
3 return
與String s1 = "Hello";
的位元組碼編譯一模一樣,所以說結果也是一模一樣,會將He
和llo
作為一個字串整體,檢視是否已存在字元常量,無則建立。
由此得出字串通過+
拼接,與直接完整建立沒有任何差異。
String s1 = "Hello";
String s2 = "He" + "llo";
String s3 = "H" + "e" + "l" + "l" + "o";
System.out.println(s1 == s2);//true
System.out.println(s1 == s3);//true
示例四
String s1 = new String("He") + "llo";
這種場景下的字串拼接就會有些不同,我們先來看一下是如何拼接的,位元組碼如下:
0 new #2 <java/lang/StringBuilder>
3 dup
4 invokespecial #3 <java/lang/StringBuilder.<init> : ()V>
7 new #4 <java/lang/String>
10 dup
11 ldc #5 <He>
13 invokespecial #6 <java/lang/String.<init> : (Ljava/lang/String;)V>
16 invokevirtual #7 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
19 ldc #8 <llo>
21 invokevirtual #7 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
24 invokevirtual #9 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>
27 astore_1
28 return
很顯然,Java在底層是通過StringBuilder
來進行字串的拼接,通過append()
方法,最後直接toString()
返回。
那麼我們結合之前講到的,推斷一下,這種場景會建立幾個物件呢?
- 建立
StringBuilder
物件 -
new String("He")
建立1或者2個物件,過程直接參考示例一 -
+ "llo"
建立0或1個物件,過程參考示例二和示例三
總結: 如果字串常量池中沒有He
和llo
,則會建立4個物件。如果已經存在常量,則建立2個物件
示例五 通過new 建立String的坑
String s1 = "1" + "1";
String s2 = "11";
System.out.println(s1 == s2);
通過以上示例二和三我們知道結果為true
,因為"1" + "1"會建立一個11
字串物件,s2在建立時11
已經存在在字串常量池
,所以直接獲取該引用。
接下來我們換一種方式
String s1 = new String("1") + new String("1");
String s2 = "11";
System.out.println(s1 == s2);
那麼這種方式的結果呢?大家可以試一下,並且思考一下,其實在示例四中已經有了答案,但這裡我需要再強調一下,因為這是一個很容易忽略卻很影響結果的要點。
結果:false
那麼為什麼呢? 原因如下:
-
"1" + "1";
這種會直接建立一個值為11
的字串物件,並且會將該物件快取到字串常量池
。 -
new String("1") + new String("1");
而這種方式是通過StringBuilder
進行拼接的,他是通過建立兩個1
字串進行拼接,最終結果並沒有建立11
這個字串物件,所以這種方式建立後,字串常量池
中並沒有11
字元常量存在。
再來回顧一下String s1 = "1" + "1";
的位元組碼內容
0 ldc #2 <11>
2 astore_1
3 return
顯然,這種方式最終建立了11
字串物件,並且會快取到字串常量池
我們來再次看一下String s1 = new String("1") + new String("1");
的位元組碼檔案,來回顧一下
0 new #2 <java/lang/StringBuilder>
3 dup
4 invokespecial #3 <java/lang/StringBuilder.<init> : ()V>
7 new #4 <java/lang/String>
10 dup
11 ldc #5 <1>
13 invokespecial #6 <java/lang/String.<init> : (Ljava/lang/String;)V>
16 invokevirtual #7 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
19 new #4 <java/lang/String>
22 dup
23 ldc #5 <1>
25 invokespecial #6 <java/lang/String.<init> : (Ljava/lang/String;)V>
28 invokevirtual #7 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
31 invokevirtual #8 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>
34 astore_1
35 return
看這段位元組碼,這段位元組碼執行了兩次,分別建立兩個String物件,值都是為1。並沒有建立11
字串物件。
7 new #4 <java/lang/String>
10 dup
11 ldc #5 <1>
總結:
底層通過StringBuilder
拼接的字串,最終的結果不會建立一個新的字串物件。
詳細描述:String s1 = "1" + "1"
會直接建立一個值為11
字串,然後將引用放入字串常量池
String s2 = "11"
然後我們再宣告一個值為11
字串,其實它會在字串常量池
中獲取s1的引用,所以它們的引用相等。
String s1 = new String("1") + new String("1")
只會建立一個值為1
的字串,然後將引用放入字串常量池
。String s2 = "11"
建立一個值為11
的字串,由於字串常量池
中不存在11
常量,則會重新建立一個新的11
字串物件,然後將引用放入字串常量池
,所以它們的引用不相等。
類似的案例如下:
String s1 = new String("1") + new String("1");
String s2 = new String("1") + "1";
String s3 = new StringBuilder("1").append("1").toString();
String s4 = "1" + "1";
System.out.println(s1 == s4); //false
System.out.println(s2 == s4);//false
System.out.println(s3 == s4);//false
結果統統為false
,因為它們底層都是由StringBuilder
拼接,不會產生11
字串物件。
示例六
String s1 = new String("1") + new String("1");
s1.intern();
String s2 = "11";
System.out.println(s1 == s2);
通過示例五的學習,我們知道,這裡應該會輸出false
,但這裡結果為true
,主要原因就是s1執行了intern()
方法.intern()
作用: 主動將字串自身的引用新增到字串常量池
,如果字串常量池中已存在,則直接返回已存在的引用,不存在則新增。