問題概括
靜態常量可以再編譯器確定字面量,但常量並不一定在編譯期就確定了, 也可以在運行時確定,所以Java針對某些情況制定了常量優化機制。
常量優化機制
- 給一個變量賦值,如果等於號的右邊是常量的表達式並且沒有一個變量,那么就會在編譯階段計算該表達式的結果。
- 然后判斷該表達式的結果是否在左邊類型所表示范圍內。
- 如果在,那么就賦值成功,如果不在,那么就賦值失敗。
注意如果一旦有變量參與表達式,那么就不會有編譯期間的常量優化機制。
結合問題,我們就可以大致猜出,如果常量能在編譯期確定就會有優化,不能的話就不存在。
下面我們來詳細講解一下這個機制,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明顯要調用函數才能計算出來的,是要在運行時才能確定結果,那肯定必須的開辟內存創建新的對象。具體就是通過黃色字體所描述的方法
