☕【Java技術指南】「編譯器專題」重塑認識Java編譯器的執行過程(常量優化機制)!


問題概括

靜態常量可以再編譯器確定字面量,但常量並不一定在編譯期就確定了, 也可以在運行時確定,所以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明顯要調用函數才能計算出來的,是要在運行時才能確定結果,那肯定必須的開辟內存創建新的對象。具體就是通過黃色字體所描述的方法


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM