首先看一下這道常見的面試題,下面程式碼中,會建立幾個字串物件?
String s="a"+"b"+"c";
如果你比較一下Java原始碼和反編譯後的位元組碼檔案,就可以直觀的看到答案,只建立了一個String物件。
估計大家會有疑問了,為什麼原始碼中字串拼接的操作,在編譯完成後會消失,直接呈現為一個拼接後的完整字串呢?
這是因為在編譯期間,應用了編譯器優化中一種被稱為常量摺疊(Constant Folding)的技術,會將編譯期常量的加減乘除的運算過程在編譯過程中摺疊。編譯器通過語法分析,會將常量表示式計算求值,並用求出的值來替換表示式,而不必等到執行期間再進行運算處理,從而在執行期間節省處理器資源。
而上邊提到的編譯期常量的特點就是它的值在編譯期就可以確定,並且需要完整滿足下面的要求,才可能是一個編譯期常量:
- 被宣告為
final
- 基本型別或者字串型別
- 宣告時就已經初始化
- 使用常量表示式進行初始化
上面的前兩條比較容易理解,需要注意的是第三和第四條,通過下面的例子進行說明:
final String s1="hello "+"Hydra";
final String s2=UUID.randomUUID().toString()+"Hydra";
編譯器能夠在編譯期就得到s1
的值是hello Hydra
,不需要等到程式的執行期間,因此s1
屬於編譯期常量。而對s2
來說,雖然也被宣告為final
型別,並且在宣告時就已經初始化,但使用的不是常量表示式,因此不屬於編譯期常量,這一型別的常量被稱為執行時常量。再看一下編譯後的位元組碼檔案中的常量池區域:
可以看到常量池中只有一個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 class
1
如果對上面進行修改,對變數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"));
}
執行結果:
true
false
程式碼中字串h1
和h2
都使用常量賦值,區別在於是否使用了final
進行修飾,對比編譯後的程式碼,s1
進行了摺疊而s2
沒有,可以印證上面的理論,final
修飾的字串變數屬於編譯期常量。
再看一段程式碼,執行下面的程式,結果會返回什麼呢?
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");
}
結果列印:
false
true
為什麼會出現不同的結果?在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
對位元組碼檔案進行反編譯,可以看到在這一過程中,編譯器同樣會進行優化:
可以看到,雖然我們在程式碼中沒有顯示的呼叫StringBuilder
,但是在字串拼接的場景下,Java編譯器會自動進行優化,新建一個StringBuilder
物件,然後呼叫append
方法進行字串的拼接。而在最後,呼叫了StringBuilder
的toString
方法,生成了一個新的字串物件,而不是引用的常量池中的常量。這樣,也就能解釋為什麼在上面的例子中,s2=="ab"
會返回false
了。
本文程式碼基於Java 1.8.0_261-b12 版本測試
首先看一下這道常見的面試題,下面程式碼中,會建立幾個字串物件?
String s="a"+"b"+"c";
如果你比較一下Java原始碼和反編譯後的位元組碼檔案,就可以直觀的看到答案,只建立了一個String物件。
估計大家會有疑問了,為什麼原始碼中字串拼接的操作,在編譯完成後會消失,直接呈現為一個拼接後的完整字串呢?
這是因為在編譯期間,應用了編譯器優化中一種被稱為常量摺疊(Constant Folding)的技術,會將編譯期常量的加減乘除的運算過程在編譯過程中摺疊。編譯器通過語法分析,會將常量表示式計算求值,並用求出的值來替換表示式,而不必等到執行期間再進行運算處理,從而在執行期間節省處理器資源。
而上邊提到的編譯期常量的特點就是它的值在編譯期就可以確定,並且需要完整滿足下面的要求,才可能是一個編譯期常量:
- 被宣告為
final
- 基本型別或者字串型別
- 宣告時就已經初始化
- 使用常量表示式進行初始化
上面的前兩條比較容易理解,需要注意的是第三和第四條,通過下面的例子進行說明:
final String s1="hello "+"Hydra";
final String s2=UUID.randomUUID().toString()+"Hydra";
編譯器能夠在編譯期就得到s1
的值是hello Hydra
,不需要等到程式的執行期間,因此s1
屬於編譯期常量。而對s2
來說,雖然也被宣告為final
型別,並且在宣告時就已經初始化,但使用的不是常量表示式,因此不屬於編譯期常量,這一型別的常量被稱為執行時常量。再看一下編譯後的位元組碼檔案中的常量池區域:
可以看到常量池中只有一個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 class
1
如果對上面進行修改,對變數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"));
}
執行結果:
true
false
程式碼中字串h1
和h2
都使用常量賦值,區別在於是否使用了final
進行修飾,對比編譯後的程式碼,s1
進行了摺疊而s2
沒有,可以印證上面的理論,final
修飾的字串變數屬於編譯期常量。
再看一段程式碼,執行下面的程式,結果會返回什麼呢?
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");
}
結果列印:
false
true
為什麼會出現不同的結果?在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
對位元組碼檔案進行反編譯,可以看到在這一過程中,編譯器同樣會進行優化:
可以看到,雖然我們在程式碼中沒有顯示的呼叫StringBuilder
,但是在字串拼接的場景下,Java編譯器會自動進行優化,新建一個StringBuilder
物件,然後呼叫append
方法進行字串的拼接。而在最後,呼叫了StringBuilder
的toString
方法,生成了一個新的字串物件,而不是引用的常量池中的常量。這樣,也就能解釋為什麼在上面的例子中,s2=="ab"
會返回false
了。
本文程式碼基於Java 1.8.0_261-b12 版本測試
如果文章對您有所幫助,歡迎關注公眾號 碼農參上