一道String字符串比較問題引發的字節碼分析
public class a {
public static void main(String[] args)throws Exception{
}
public static void aa(){
String s1="a";//內存在方法區的常量池
String s2="b";//內存在方法區的常量池
String s12 = "ab";//內存在方法區的常量池
String s3 = s1 + s2;//s3的內存所在???
p(s3==s12);//false
}
public static void bb(){
String s1="a"+"b";//s1的內存所在???
String s2 = "ab";//內存在方法區的常量池
p(s1==s2);//true
}public static void p(Object obj){
System.out.println(obj);
}
}
這是我們經常碰到的煩人的String比較問題,要得到答案,就要弄清楚aa()方法中的s3的內存在哪裡?,和bb()方法中的s1的內存在哪裡?
不多說,貼上a.class文件反編譯的字節碼指令:
首先是 aa()方法:
public static void aa();
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=0 共4個本地變量空間
0: ldc #3 // String a 將字符串"a"從常量池中推送至棧頂
2: astore_0 將棧頂引用類型(即字符串"a")存入第一個本地變量
3: ldc #4 // String b 將字符串"b"從常量池推送至棧頂
5: astore_1 將棧頂引用類型(即字符串"b")存入第二個本地變量
6: ldc #5 // String ab 將字符串"ab"從常量池推送至棧頂
8: astore_2 將棧頂引用類型(即字符串"ab")存入第三個本地變量
9: new #6 // class java/lang/StringBuilder 創建StringBuilder對象sb,並將引用值壓入棧頂
12: dup 復制棧頂數值,並將復制值壓入棧頂
13: invokespecial #7 // Method java/lang/StringBuilder. 調用對象的初始化方法
"<init>":()V
16: aload_0 將第一個本地變量(即字符串"a")推送至棧頂
17: invokevirtual #8 // Method java/lang/StringBuilder. 調用實例方法sb.append("a");
append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
20: aload_1 將第二個本地變量(即字符串"b")推送至棧頂
21: invokevirtual #8 // Method java/lang/StringBuilder. 調用實例方法sb.append("b");
append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: invokevirtual #9 // Method java/lang/StringBuilder. 調用實例方法sb.toString(),並將結果【Java堆地址】放在棧頂
toString:()Ljava/lang/String;
27: astore_3 將棧頂引用類型(即堆地址)存入第四個本地變量
28: aload_3 將第四個本地變量(即堆地址)推送至棧頂
29: aload_2 將第三個本地變量(即字符串"ab")推送至棧頂
30: if_acmpne 37 比較棧頂兩引用數值,結果不同跳轉(當然不同啦)
33: iconst_1
34: goto 38
37: iconst_0 將int類型 0 壓入棧頂
38: invokestatic #10 // Method java/lang/Boolean.valueO 調用靜態方法Boolean.valueOf();實現基本數據類型->包裝類型自動轉換
f:(Z)Ljava/lang/Boolean;
41: invokestatic #11 // Method p:(Ljava/lang/Object;)V 調用靜態方法p(false);//輸出false
44: return 從當前方法返回void
針對其中的一些解釋:(下面的數字是字節碼偏移量)
24 為何在sb.toString()我說的是【堆地址】,大家看源碼就知道了。
//這是StringBuilder的源碼,返回的是堆上的字符串地址
public String toString() {
return new String(value, 0, count);
}
所以在aa()方法中,s3的內存其實在Java堆上,s12在方法區的常量池上,所以兩者不相等。
37 boolean到底分配幾個字節,在這裡大家可以看到。
如果為true,編譯器翻譯的字節碼是iconst_1,意思將int類型1存入棧頂,所以單個引用boolean值時,分配4個字節,和int相同。(數組boolean沒測試,不清楚)
如果為false,編譯器翻譯的字節碼是iconst_0,意思將int類型0存入棧頂。
38 在這裡我們還能看到自動類型轉換的身影,這裡是基本數據類型boolean->包裝類Boolean的自動類型轉換,實際調用的就是Boolean.valueOf()靜態方法,這是因為下面的p()方法裡面需要的是Object引用類型,所以進行了自動類型轉換。
然後是 bb()方法:
public static void bb();
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=0 兩個本地變量空間
0: ldc #5 // String ab 將字符串"ab"從常量池中推送至棧頂
2: astore_0 將棧頂引用類型(字符串"ab")存入第一個本地變量
3: ldc #5 // String ab 將字符串"ab"從常量池中推送至棧頂
5: astore_1 將棧頂引用類型(字符串"ab")存入第一個本地變量
6: aload_0 將第一個本地變量("ab")推送至棧頂
7: aload_1 將第二個本地變量("ab")推送至棧頂
8: if_acmpne 15 比較棧頂兩引用類型數值,結果不同跳轉(這裡當然相同啦)
11: iconst_1 將int類型 1 推送至棧頂
12: goto 16 無條件跳轉到16字節碼偏移量
15: iconst_0
16: invokestatic #10 // Method java/lang/Boolean.valueO 調用靜態方法Boolean.valueOf();並將返回的Boolean類型的true壓入棧頂
f:(Z)Ljava/lang/Boolean;
19: invokestatic #11 // Method p:(Ljava/lang/Object;)V 調用靜態方法p(true);輸出true
22: return 從當前方法返回void
針對其中的一些解釋:(下面的數字是字節碼偏移量)
0 大家看到了吧,編譯器看到String a="aa"+"bb";會自動合並,將"aabb"存入常量池,並返回地址。所以答案為true。