1 | /* |
Answer
1 | true |
這是什麼巫術 ?
首先,在Java中的==
有兩種意涵:
對於基本資料型態,比對兩者的數值
對於物件,則是比對兩者存放的位置
String是個物件,所以對String使用==,我們其實是在比較物件的記憶體位置,這就解決了最後一個問題,s3與s10都是經過new
配置而成,實際上new傳回了兩個String,只不過他們的字串值都叫做Hello而已,比對兩者的位置當然會得出false。
stack | heap |
---|---|
s3 -> | “Hello” |
s10-> | “Hello” |
如果明白了最後一題在搞什麼花樣,問題來了,為什麼第一題是true?
常數池
讓我們從答案往回推,既然第一題是true,也就代表s1與s2所指向的是同一個物件,而為何會指向同一個物件呢?
其實是Java存在一個特殊空間,把編譯時期就已經確定的常數(constant)包進.class,執行時再從這個空間統一拿出。這個特殊空間稱作常數池或常量池(constant pool),它主要保管這兩種東西:
字面量(Literal)
包含了常數以及既定字串。Symbolic References
主要是類別和介面的Fully qualified name,欄位和方法的名稱。
每當編譯完成,編譯器會將上述兩者包進.class中。原始碼中每一個字面量會被統一構成一張常數表,並透過索引的型式參照它。而當JVM載入這份.class檔時,會特別為常數池在方法區建立一個資料結構,並把常數表中所有的字面量在Heap上生成一個實例(即是String.intern()的工作內容)。
除了String,Java對double
及float
以外的6個基本類別在區間-128~127
都實作了常數池,也就是說
1 | Integer i1 = 1; |
關於這點,我們也可從Integer類別中valueOf的實作得到證實:
1 | //IntegerCache.low 預設為 -128 |
我會提到valueOf是因為
Integer i1 = 1
其實是個編譯器蜜糖,編譯器會將上述程式碼展開成Integer i1 = valueOf(1)
,對於這部分更深入的細節,可以餵食Google「Java裝箱拆箱」。一般來說,常數池是這麼運作的(以string及題目為例):
- 編譯時發現字串,若常數池中已存在,則將現有的(先前找到的)位置賦予它,否則將該字串壓入常數池,並紀錄位置
- 當載入class時,JVM會尋找.class的常數表,發現了字串常量
Hello
,在heap中生成對應的實例。 - 接著JVM會對常數池的入口位置進行解析,首先會找到字串
s1
的字面量Hello
。 - 由於常數池中已經存在
Hello
,所以提供常數池的引用給s1
- 之後JVM找到
s2
的字面量Hello
, - 由於常數池中已經存在
Hello
,所以仍舊將s2
指向已經生成好的Hello
,而非再產生一個Hello
。
由此我們也能發現常數池的優點,它達到了共享的效果,避免資源頻繁產生或釋放造成不必要的浪費。
關於第二題 (s1==s3) 為false,是因為new是動態產生新的物件,其跟s1接收到的Hello自然是不同的,但是要注意,new 仍舊會觸發常數池存取,如果今天s3 = new String("Hello");
是第一行,常數池的處理會變成這樣:
- 當載入class時,JVM會尋找.class的常數表,發現了字串常量
Hello
,在heap中生成對應的實例。 - 因為 s3 使用的是 new ,所以其不引用常數池,而是在heap生成一個新的String物件
- 所以此時heap實際上存在兩份物件,一個是讓常數池提供引用的,另一個是s3持有的。
直觀來看,= 與 new 都會進行常數池的檢查,就像先前說的,編譯器找的是原始碼中的字面量,它才不會管你是 new 還是 = 。不過在執行時, = 會引用常數池,new則是另外生成新的物件。
關於第三題 (s1==s4) 為true,是Java的優化功能之一,編譯器會將"Hel"+"lo"
組合成"Hello"
,因此道理和(s1==s2)相同。
關於第四題,近似於第二題的概念,具體來說String s5 = "Hel" + new String("lo");
做了三件事:
- 常數池存放了 “Hel” 及 “lo”,並產生對應的實例
- heap 再另外new出一個新的 “lo”
- 使用String方法.append()合併”Hel”與”lo”,傳回一個新字串給s5
第五題則是和第三題的類比,會回傳false可不是因為它是拆成He與llo,而是跟先前提到的優化有關,它只會進行常數的優化,這是因為編譯器不知道我們在String s9 = s7+s8;
這行前,會不會對s7
與s8
的值進行修改,若貿然的假設s7
仍是He或s8
仍是llo,反而可能導致預期外的錯誤,這風險可不是編譯器能承擔的,自然也不會負責這個部分。