一道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。