問題概括

靜態常量可以再編譯器確定字面量,但常量並不一定在編譯期就確定了, 也可以在執行時確定,所以Java針對某些情況制定了常量優化機制。

常量優化機制

  1. 給一個變數賦值,如果等於號的右邊是常量的表示式並且沒有一個變數,那麼就會在編譯階段計算該表示式的結果。
  2. 然後判斷該表示式的結果是否在左邊型別所表示範圍內。
  3. 如果在,那麼就賦值成功,如果不在,那麼就賦值失敗。

注意如果一旦有變數參與表示式,那麼就不會有編譯期間的常量優化機制。

結合問題,我們就可以大致猜出,如果常量能在編譯期確定就會有優化,不能的話就不存在。

下面我們來詳細講解一下這個機制,Java中的常量池常量優化機制主要是兩方面

就是對於byte/short/char三種類型的常量優化機制

先貼出一張Java八大資料型別大小範圍表以供參考:

以下面這個程式為例
byte b1  = 1 + 2;
System.out.println(b1);
// 輸出結果 3
執行結果解釋:

1和2都是常量,Java有常量優化機制,就是可以編譯時可以明顯確定常量結果,所以直接把1和2的結果賦值給b1了。(和直接賦值3是一個意思)

換一種情況看看,把右邊常量改成變數
byte b1  = 3;
byte b2 = 4;
byte b3 = b1 + b2;
System.out.println(b3); // 程式報錯

程式報錯了,意思說型別不匹配:無法從int轉換為byte

解釋原因,從兩個方面:
  • byte 與 byte (或者 short char ) 進行運算的時候會提升int 兩個int 型別相加的結果也是int 型別

  • b1 和 b2 是兩個變數,變數儲存的是變化,在編譯的時候無法判斷裡面的值,相加有可能會超出byte的取值這就是為什麼一旦有變數參與表示式,那麼就不會有編譯期間的常量優化機制。

在這裡我們試著把變數新增final改回常量,看看又有什麼結果

final byte b1  = 1;
final byte b2 = 2;
byte b3 = b1 + b2;
System.out.println(b3);

發現程式可以正常執行,輸出結果為3,所以可知常量優化機制一定是針對常量的。

接下來我們再看另外一個程式
byte b1  = 127 + 2;
System.out.println(b4);

程式再次報錯,同樣也是型別不匹配:無法從int轉換為byte,這裡解釋一下,byte取值範圍為-128~127;很明顯右邊表示式的結果是否在左邊型別所表示範圍,這個就是導致此錯誤出現的原因。

某些場景下,取值範圍大的資料型別(int)可以直接賦值給取值範圍小的(byte、shor、char),而且只能特定int賦值給byte/short/char,其他基本資料型別不行,如下圖。

int num1 = 10;
final int num2 = 10;
byte var1 = num1 + 20; // 存在變數,編譯報錯
byte var2 = num2 + 20; // 編譯通過

這個也是常量優化機制的一部分

所以我們這裡總結一下byte/short/char三種類型的常量優化機制

  • 先判斷值是否是常量, 然後再看值是否在該資料型別的取值範圍內
  • 只有byte, short, char 可以使用常量優化機制,轉換成int型別(這個你換成其他基本資料型別就不適應了)來個程式測試一下,下面這個就是單純把之前的byte改成了int型,發現並不像之前報錯,反而正常執行,輸出結果3,所以就說明了只有byte, short, char 可以使用常量優化機制
int a = 1;
int b = 2;
int c = a + b;
System.out.println(c);
拓展一下(易錯點):
byte var = 10;
var = var + 20; // 編譯報錯,運算中存在變數
var += 20; // 等效於: var = (short) (var + 20); 沒有走常量優化機制,而是進行了型別轉換

就是對於編譯器對String型別優化(這個是重點難點)

String s1 = "abc";
String s2 = "a"+"b"+"c";
System.out.println(s1 == s2);
  • 這個輸出的結果是多少呢?有人就會認為 “a” + “b”+“c"會生成新的物件"abc”,但是這個物件和String s2 = "abc"不同,(a == b)是比較物件引用,因此不相等,結果為false。

  • 如果你是這樣想的話,那恭喜你對java的String有一定了解,但是你不清楚Java的常量池常量優化機制。

這個程式碼正確輸出結果為true!!!

那麼到底為什麼呢,下面就來解釋一下原因:

String s2 = “a” + “b”+“c”;編譯器將這個"a" + “b”+“c"作為常量表達式,在編譯時進行優化,直接取表示式結果"abc”,這裡沒有建立新的物件,而是從JVM字串常量池中獲取之前已經存在的"abc"物件。因此a,b具有對同一個string物件的引用,兩個引用相等,結果true。

意思是說先通過優化,程式碼簡化為

String s1 = "abc";
String s2 = "abc";
System.out.println(s1 == s2);

再基於jvm對String的處理機制的基礎上,得出true的結論。

下面進一步探討,什麼樣的String + 表示式會被編譯器當成常量表達式?

String b = "a" + "b";

這個String + String被正式是ok的,那麼string + 基本型別呢?

String a = "a1";
String b = "a" + 1;
System.out.println((a == b)); //result = true
String a = "atrue";
String b = "a" + true;
System.out.println((a == b)); //result = true
String a = "a3.4";
String b = "a" + 3.4;
System.out.println((a == b)); //result = true

可見編譯器對String + 基本型別是當成常量表達式直接求值來優化的。

既然常量弄完了,我們換成變數來試試

String s1 = "ab";
String s2 = "abc";
String s3 = s1 + "c";
System.out.println(s3 == s2);
輸出的結果是false

這裡我們就可以看到常量優化只是針對常量,如果有變數的話就不能被優化

執行原理
  • String s3 = s1+“c”;這一句話,是在StringBuffer緩衝區中進行建立一個StringBuffer物件,將兩者相加。

  • 但是對s3進行賦值時不能夠直接將緩衝區的物件地址取來而是用toString方法變成另外的堆記憶體,然後賦值給s3,所以,s3和s2的地址值已經不同了,所以輸出false。

這裡我們還可以拓展一下,把s1前面加final修飾符修改為常量看看

final String s1 = "ab";
String s2 = "abc";
String s3 = s1 + "c";
System.out.println(s2 == s3);

輸出的結果居然變成了true,看來只要是進入常量池的常量,就有可能存在常量優化機制

再往裡走一點,觀察下面程式

private static String getS() {
return "b";
} String s1 = "abc";
String s2 = "a"+getS();
System.out.println((s1 == s2));

結果又是出人意料,竟然是false

執行原理

編譯器發現s2值是要呼叫函式才能計算出來的,是要在執行時才能確定結果的,所以編譯器就設定為執行時執行到String s3=“a” + getS();時 要重新分配記憶體空間,導致s2和s1是指向兩個不同的記憶體地址,所以==比較結果為false;

看來String這個所謂的"物件",完全不可以看成一般的物件,Java對String的處理近乎於基本型別,最大限度的優化了幾乎能優化的地方。

我們來舉個例子總結一下上面所有內容

public static void main(String[] arge) {

        //1
String str1 = new String("1234");
String str2 = new String("1234");
System.out.println("new String()==:" + (str1 == str2)); //2
String str3 = "1234";
String str4 = "1234";
System.out.println("常量字串==:" + (str3 == str4)); //3
String str5 = "1234";
String str6 = "12" + "34";
System.out.println("常量表達式==:" + (str5 == str6)); //4
String str7 = "1234";
String str8 = "12";
String str9 = str8 + "34";
System.out.println("字串和變數相加的表示式==:" + (str7 == str9)); //5
final String val = "34";
String str10 = "1234";
String str11 = "12" + val;
System.out.println("字串和常量相加的表示式==:" + (str10 == str11)); //6
String str12 = "1234";
String str13 = "12" + 34;
System.out.println("字串和數字相加的表示式==:" + (str12 == str13)); //7
String str14 = "12true";
String str15 = "12" + true;
System.out.println("字串和Boolen相加表示式==:" + (str14 == str15)); //8
String str16 = "1234";
String str17 = "12" + getVal();
System.out.println("字串和函式得來的常量相加表示式==:" + (str16 == str17));
}
private static String getVal()
{
return "34";
}

執行輸出:

new String()==:false
常量字串==:true
常量表達式==:true
字串和變數相加的表示式==:false
字串和常量相加的表示式==:true
字串和數字相加的表示式==:true
字串和Boolen相加表示式==:true
字串和函式得來的常量相加表示式==:false

程式碼分析:

Java中,String是引用型別;是關係運算符,比較兩個引用型別時,判斷的依據是:雙方是否是指向了同一個記憶體地址。

  • (1)String為引用型別,str1和str2為新例項化出來的物件,分別指向不同的記憶體地址。而==對於引用型別判斷,是判斷的是引用地址,所以例子1結果為false。

  • (2)對於第二個例子,編譯器編譯程式碼時,會將”1234”當做一個常量,並儲存在JVM的常量池中,然後編譯String str3=”1234”;時,將常量的指標賦值給str3,在編譯String str4=”1234”;時,編譯器查詢常量池裡有沒有值相同的常量,如果有就將存在的常量賦給str4,這樣結果就是str3和str4都指向了常量池中的常量的地址,所以==比較結果為true;

  • (3)第三個例子,編譯時編譯器發現能夠計算出”12”+”34”的值,它是個常量,就按照第二個例子一樣處理,最終str5和str6都指向了同一個記憶體地址。所以==比較結果為true;

  • (4)第四個例子,常量優化只針對常量,String str9 = str8 + “34”;這一句話,str9的值在執行時才能確定結果,是在StringBuffer緩衝區中進行建立一個StringBuffer物件,將兩者相加。但是對s3進行賦值時不能夠直接將緩衝區的物件地址取來而是用toString方法變成另外的堆記憶體,然後賦值給s3,所以,s3和s2的地址值已經不同了,所以輸出false。

  • (5)第五個例子、第六個例子和第七個例子,類似第三個例子,編譯時編譯器發現能夠計算出值,就儘量計算出來,所以==比較結果為true;

  • (6)第八個例子中,編譯器發現str17值是要呼叫函式才能計算出來的,是要在執行時才能確定結果的,所以編譯器就設定為執行時執行到String str17=“12” + getVal();時 要重新分配記憶體空間,導致str13和str1是指向兩個不同的記憶體地址,所以==比較結果為false;

總結一下

Java語言為字串連線運算子(+)提供特殊支援,併為其他物件轉換為字串。 字串連線是通過StringBuilder (或StringBuffer )類及其append方法實現的。 字串轉換是通過方法來實現toString(JDK1.8 api文件) 。(toString方法返回值是String,所以會返回一個String物件)。由於String的不可變性,對其進行操作的效率會大大降低,但對 “+”操作符,編譯器對其進行了優化,往通俗來講,如果編譯時能直接得到最終字串的結果就儘量獲得最後的字串,這樣就免於中間建立物件的浪費了。

String str = "a" + "b" + "c";  // 直接等價於 str = "abc";
// 這個就解釋了上面為true的所有情況

如果不能直接計算得到最終的字串,就像上面的例子4一樣,str17明顯要呼叫函式才能計算出來的,是要在執行時才能確定結果,那肯定必須的開闢記憶體建立新的物件。具體就是通過黃色字型所描述的方法