首先看一下這道常見的面試題琅关,下面代碼中煮岁,會創(chuàng)建幾個字符串對象?
String s="a"+"b"+"c";
如果你比較一下Java源代碼和反編譯后的字節(jié)碼文件涣易,就可以直觀的看到答案画机,只創(chuàng)建了一個String對象。
估計大家會有疑問了都毒,為什么源代碼中字符串拼接的操作色罚,在編譯完成后會消失,直接呈現(xiàn)為一個拼接后的完整字符串呢账劲?
這是因為在編譯期間戳护,應(yīng)用了編譯器優(yōu)化中一種被稱為常量折疊(Constant Folding)的技術(shù),會將編譯期常量的加減乘除的運算過程在編譯過程中折疊瀑焦。編譯器通過語法分析腌且,會將常量表達式計算求值,并用求出的值來替換表達式榛瓮,而不必等到運行期間再進行運算處理铺董,從而在運行期間節(jié)省處理器資源。
而上邊提到的編譯期常量的特點就是它的值在編譯期就可以確定禀晓,并且需要完整滿足下面的要求精续,才可能是一個編譯期常量:
- 被聲明為
final
- 基本類型或者字符串類型
- 聲明時就已經(jīng)初始化
- 使用常量表達式進行初始化
上面的前兩條比較容易理解,需要注意的是第三和第四條粹懒,通過下面的例子進行說明:
final String s1="hello "+"Hydra";
final String s2=UUID.randomUUID().toString()+"Hydra";
編譯器能夠在編譯期就得到s1
的值是hello Hydra
重付,不需要等到程序的運行期間,因此s1
屬于編譯期常量凫乖。而對s2
來說确垫,雖然也被聲明為final
類型,并且在聲明時就已經(jīng)初始化帽芽,但使用的不是常量表達式删掀,因此不屬于編譯期常量,這一類型的常量被稱為運行時常量导街。再看一下編譯后的字節(jié)碼文件中的常量池區(qū)域:
可以看到常量池中只有一個String
類型的常量hello Hydra
披泪,而s2
對應(yīng)的字符串常量則不在此區(qū)域。對編譯器來說搬瑰,運行時常量在編譯期間無法進行折疊付呕,編譯器只會對嘗試修改它的操作進行報錯處理计福。
另外值得一提的是,編譯期常量與運行時常量的另一個不同就是是否需要對類進行初始化徽职,下面通過兩個例子進行對比:
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 class
1
如果對上面進行修改姆钉,對變量a
添加final
進行修飾:
public static final int a=1;
再次執(zhí)行上面的代碼说订,會輸出:
1
可以看到在添加了final
修飾后,兩次運行的結(jié)果是不同的潮瓶,這是因為在添加final
后陶冷,變量a
成為了編譯期常量,不會導致類的初始化毯辅。另外埂伦,在聲明編譯器常量時,final
關(guān)鍵字是必要的思恐,而static
關(guān)鍵字是非必要的沾谜,上面加static
修飾只是為了驗證類是否被初始化過。
我們再看幾個例子來加深對final
關(guān)鍵字的理解胀莹,運行下面的代碼:
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"));
}
執(zhí)行結(jié)果:
true
false
代碼中字符串h1
和h2
都使用常量賦值基跑,區(qū)別在于是否使用了final
進行修飾,對比編譯后的代碼描焰,s1
進行了折疊而s2
沒有媳否,可以印證上面的理論,final
修飾的字符串變量屬于編譯期常量荆秦。
再看一段代碼篱竭,執(zhí)行下面的程序,結(jié)果會返回什么呢步绸?
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
修飾,但是初始化時沒有使用編譯期常量靡努,因此它也不是編譯期常量坪圾。
在上面的一些例子中晓折,在執(zhí)行常量折疊的過程中都遵循了使用常量表達式進行初始化這一原則惑朦,這里可能有的同學還會有疑問,到底什么樣才能算得上是常量表達式呢漓概?在Oracle
官網(wǎng)的文檔中漾月,列舉了很多種情況,下面對常見的情況進行列舉(除了下面這些之外官方文檔上還列舉了不少情況胃珍,如果有興趣的話梁肿,可以自己查看):
- 基本類型和String類型的字面量
- 基本類型和String類型的強制類型轉(zhuǎn)換
- 使用
+
或-
或!
等一元運算符(不包括++
和--
)進行計算 - 使用加減運算符
+
蜓陌、-
,乘除運算符*
吩蔑、/
钮热、%
進行計算 - 使用移位運算符
>>
、<<
烛芬、>>>
進行位移操作 - ……
字面量(literals)是用于表達源代碼中一個固定值的表示法隧期,在Java中創(chuàng)建一個對象時需要使用new
關(guān)鍵字,但是給一個基本類型變量賦值時不需要使用new
關(guān)鍵字赘娄,這種方式就可以被稱為字面量仆潮。Java中字面量主要包括了以下類型的字面量:
//整數(shù)型字面量:
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");
}
結(jié)果打优志臁:
false
true
為什么會出現(xiàn)不同的結(jié)果?在Java中霹崎,String類型在使用==
進行比較時珊搀,是判斷的引用是否指向堆內(nèi)存中的同一塊地址,出現(xiàn)上面的結(jié)果那么說明指向的不是內(nèi)存中的同一塊地址尾菇。
通過之前的分析境析,我們知道s3
會進行常量折疊,引用的是常量池中的ab
派诬,所以相等劳淆。而字符串s2
在進行拼接時,表達式中引用了其他對象默赂,不屬于編譯期常量沛鸵,因此不能進行折疊。
那么,在沒有常量折疊的情況下曲掰,為什么最后返回的是false
呢疾捍?我們看一下這種情況下,編譯器是如何實現(xiàn)栏妖,先執(zhí)行下面的代碼:
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
對字節(jié)碼文件進行反編譯乱豆,可以看到在這一過程中,編譯器同樣會進行優(yōu)化:
可以看到吊趾,雖然我們在代碼中沒有顯示的調(diào)用StringBuilder
咙鞍,但是在字符串拼接的場景下,Java編譯器會自動進行優(yōu)化趾徽,新建一個StringBuilder
對象续滋,然后調(diào)用append
方法進行字符串的拼接。而在最后孵奶,調(diào)用了StringBuilder
的toString
方法疲酌,生成了一個新的字符串對象,而不是引用的常量池中的常量了袁。這樣朗恳,也就能解釋為什么在上面的例子中,s2=="ab"
會返回false
了载绿。
本文代碼基于Java 1.8.0_261-b12 版本測試