String s="a"+"b"+"c",到底建立了幾個物件?

首先看一下這道常見的面試題,下面程式碼中,會建立幾個字串物件?

String s=“a”+“b”+“c”;

如果你比較一下Java原始碼和反編譯後的位元組碼檔案,就可以直觀的看到答案,只建立了一個String物件。

String s=

估計大家會有疑問了,為什麼原始碼中字串拼接的操作,在編譯完成後會消失,直接呈現為一個拼接後的完整字串呢?

這是因為在編譯期間,應用了編譯器最佳化中一種被稱為

常量摺疊

(Constant Folding)的技術,會將

編譯期常量

的加減乘除的運算過程在編譯過程中摺疊。編譯器透過語法分析,會將常量表達式計算求值,並用求出的值來替換表示式,而不必等到執行期間再進行運算處理,從而在執行期間節省處理器資源。

而上邊提到的

編譯期常量

的特點就是它的值在編譯期就可以確定,並且需要完整滿足下面的要求,才可能是一個編譯期常量:

被宣告為

final

基本型別或者字串型別

宣告時就已經初始化

使用常量表達式進行初始化

上面的前兩條比較容易理解,需要注意的是第三和第四條,透過下面的例子進行說明:

final String s1=“hello ”+“Hydra”;final String s2=UUID。randomUUID()。toString()+“Hydra”;

編譯器能夠在編譯期就得到

s1

的值是

hello Hydra

,不需要等到程式的執行期間,因此

s1

屬於編譯期常量。而對

s2

來說,雖然也被宣告為

final

型別,並且在宣告時就已經初始化,但使用的不是常量表達式,因此不屬於編譯期常量,這一型別的常量被稱為

執行時常量

。再看一下編譯後的位元組碼檔案中的常量池區域:

String s=

可以看到常量池中只有一個

String

型別的常量

hello Hydra

,而

s2

對應的字串常量則不在此區域。對編譯器來說,執行時常量在編譯期間無法進行摺疊,編譯器只會對嘗試修改它的操作進行報錯處理。

另外值得一提的是,編譯期常量與執行時常量的另一個不同就是是否需要對類進行初始化,下面透過兩個例子進行對比:

public class IntTest1 { public static void main(String[] args) { System。out。println(a1。a); }}class a1{ static { System。out。println(“init class”); } public static int a=1;}

執行上面的程式碼,輸出:

init class1

如果對上面進行修改,對變數

a

新增

final

進行修飾:

public static final int a=1;

再次執行上面的程式碼,會輸出:

1

可以看到在添加了

final

修飾後,兩次執行的結果是不同的,這是因為在新增

final

後,變數

a

成為了編譯期常量,不會導致類的初始化。另外,在宣告編譯器常量時,

final

關鍵字是必要的,而

static

關鍵字是非必要的,上面加

static

修飾只是為了驗證類是否被初始化過。

我們再看幾個例子來加深對

final

關鍵字的理解,執行下面的程式碼:

public static void main(String[] args) { final String h1 = “hello”; String h2 = “hello”; String s1 = h1 + “Hydra”; String s2 = h2 + “Hydra”; System。out。println((s1 == “helloHydra”)); System。out。println((s2 == “helloHydra”));}

執行結果:

truefalse

程式碼中字串

h1

h2

都使用常量賦值,區別在於是否使用了

final

進行修飾,對比編譯後的程式碼,

s1

進行了摺疊而

s2

沒有,可以印證上面的理論,

final

修飾的字串變數屬於編譯期常量。

String s=

再看一段程式碼,執行下面的程式,結果會返回什麼呢?

public static void main(String[] args) { String h =“hello”; final String h2 = h; String s = h2 + “Hydra”; System。out。println(s==“helloHydra”);}

答案是

false

,因為雖然這裡字串

h2

final

修飾,但是初始化時沒有使用編譯期常量,因此它也不是編譯期常量。

在上面的一些例子中,在執行常量摺疊的過程中都遵循了

使用常量表達式進行初始化

這一原則,這裡可能有的同學還會有疑問,到底什麼樣才能算得上是常量表達式呢?在

Oracle

官網的文件中,列舉了很多種情況,下面對常見的情況進行列舉(除了下面這些之外官方文件上還列舉了不少情況,如果有興趣的話,可以自己檢視):

基本型別和String型別的字面量

基本型別和String型別的強制型別轉換

使用

+

-

等一元運算子(不包括

++

——

)進行計算

使用加減運算子

+

-

,乘除運算子

*

/

%

進行計算

使用移位運算子

>>

<<

>>>

進行位移操作

……

字面量(literals)是用於表達原始碼中一個固定值的表示法,在Java中建立一個物件時需要使用

new

關鍵字,但是給一個基本型別變數賦值時不需要使用

new

關鍵字,這種方式就可以被稱為字面量。Java中字面量主要包括了以下型別的字面量:

//整數型字面量:long l=1L;int i=1;//浮點型別字面量:float f=11。1f;double d=11。1;//字元和字串型別字面量:char c=‘h’;String s=“Hydra”;//布林型別字面量:boolean b=true;

當我們在程式碼中定義並初始化一個字串物件後,程式會在常量池(

constant pool

)中快取該字串的字面量,如果後面的程式碼再次用到這個字串的字面量,會直接使用常量池中的字串字面量。

除此之外,還有一類比較特殊的

null

型別字面量,這個型別的字面量只有一個就是

null

,這個字面量可以賦值給任意引用型別的變數,表示這個引用型別變數中儲存的地址為空,也就是還沒有指向任何有效的物件。

那麼,如果不是使用的常量表達式進行初始化,在變數的初始化過程中引入了其他變數(且沒有被

final

修飾)的話,編譯器會怎樣進行處理呢?我們下面再看一個例子:

public static void main(String[] args) { String s1=“a”; String s2=s1+“b”; String s3=“a”+“b”; System。out。println(s2==“ab”); System。out。println(s3==“ab”);}

結果列印:

falsetrue

為什麼會出現不同的結果?在Java中,String型別在使用

==

進行比較時,是判斷的引用是否指向堆記憶體中的同一塊地址,出現上面的結果那麼說明指向的不是記憶體中的同一塊地址。

透過之前的分析,我們知道

s3

會進行常量摺疊,引用的是常量池中的

ab

,所以相等。而字串

s2

在進行拼接時,表示式中引用了其他物件,不屬於編譯期常量,因此不能進行摺疊。

那麼,在沒有常量摺疊的情況下,為什麼最後返回的是

false

呢?我們看一下這種情況下,編譯器是如何實現,先執行下面的程式碼:

public static void main(String[] args) { String s1=“my ”; String s2=“name ”; String s3=“is ”; String s4=“Hydra”; String s=s1+s2+s3+s4;}

然後使用

javap

對位元組碼檔案進行反編譯,可以看到在這一過程中,編譯器同樣會進行最佳化:

String s=

可以看到,雖然我們在程式碼中沒有顯示的呼叫

StringBuilder

,但是在字串拼接的場景下,Java編譯器會自動進行最佳化,新建一個

StringBuilder

物件,然後呼叫

append

方法進行字串的拼接。而在最後,呼叫了

StringBuilder

toString

方法,生成了一個新的字串物件,而不是引用的常量池中的常量。這樣,也就能解釋為什麼在上面的例子中,

s2==“ab”

會返回

false

了。

本文程式碼基於Java 1。8。0_261-b12 版本測試