通過Java反編譯揭開一些問題的真相
博主在上一篇《 Java文法糖之foreach》中採用反編譯的形式進行探討進而揭開foreach文法糖的真相。進來又遇到幾個問題,通過反編譯之後才瞭解了事實的真相,覺得有必要做一下總結,也可以給各位做一下參考。
??相信很多朋友剛開始見到反編譯後的內容的時候,肯定會吐槽:WTF!其實只要靜下心來認真瞭解下,反編譯也不過如此,java位元組碼的長度為一個位元組,頂多256條指令,目前,Java虛擬機器規範已經定義了其中約200條編碼值對應的指令含義。這裡先用一個小例子來開始我們的征程(這裡只是舉例,要是在真實生活中看到這種代碼,估計要罵娘了):
int i=0;int y = i++ + ++i;i=0;int z = i++ + ++i + ++i + ++i + i++ + ++i;
問題來了:最後y和z分別是多少?
看到y估計還能看看,看到z就暈乎乎的了,大家都知道i++是先取i值運算後對i進行自加,++i是先對i進行自加再運算。那麼在一串組合裡(y和z)怎麼運用這個規則呢。
心急的朋友估計已經開啟了編譯器,跑一跑答案不就出來了,看著結果再反推一下就知道這個“遊戲規則”了。
在C/C++和Java語言中都有這個事實:i++是先取i值運算後對i進行自加,++i是先對i進行自加再運算。但是這兩(三)種語言跑出來的結果是不一樣的。
在c/c++中(vs6):
??運行結果:
??在java中(eclipse),運行結果:2 19。
??可以看到兩(三)種語言雖然遵循了同樣的自增規則但是輸出的結果卻不一樣。這裡不探討c/c++的規則,有興趣的同學可以追根溯源。
??那麼java中遵循什麼樣的規則呢?這裡就要祭出我們的必殺器了——反編譯。
??為了防止看暈,先對這段代碼進行反編譯處理(先不看變數z):<喎?http://www.bkjia.com/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwcmUgY2xhc3M9"brush:java;">package interview;public class TestIpp{ public static void main(String[] args) { plus(); } static void plus() { int i=0; int y = i++ + ++i; System.out.println(y); }}
??對其進行反編譯,反編譯的命令如下:
首先切到當前檔案目錄下(cd命令,window和linux相同) 在目前的目錄下輸入: javac TestIpp.java (先編譯),之後會看到(window下輸入dir命令,linux下輸入ls命令)多出來一個TestIpp.class檔案 再輸入命令:javap -verbose TestIpp(反編譯,注意可以沒有.class),會看到反編譯結果。
??上面是輸入命令列的形式進行的反編譯,其實Eclipse內建了這個功能,將workspace中相應的class往eclipse的workbench上一扔即可,但是javac命令產生的class檔案eclipse無法識別。
??下面是反編譯後的代碼(篇幅限制,只顯示出plus()方法的反編譯內容):
// Method descriptor #6 ()V // Stack: 2, Locals: 2 static void plus(); 0 iconst_0 1 istore_0 [i] 2 iload_0 [i] 3 iinc 0 1 [i] 6 iinc 0 1 [i] 9 iload_0 [i] 10 iadd 11 istore_1 [y] 12 getstatic java.lang.System.out : java.io.PrintStream [21] 15 iload_1 [y] 16 invokevirtual java.io.PrintStream.println(int) : void [27] 19 return Line numbers: [pc: 0, line: 13] [pc: 2, line: 14] [pc: 12, line: 15] [pc: 19, line: 16] Local variable table: [pc: 2, pc: 20] local: i index: 0 type: int [pc: 12, pc: 20] local: y index: 1 type: int
??這裡來解析一下這些是個啥玩意兒:
0 iconst_0 *向棧頂壓入一個int常量0*,java基於棧操作,這裡首先將代碼[int i=0;]中的0壓入棧頂 1 istore_0 [i] *將棧頂元素存入本地變數0[這個變數0就是i]中*,.此時棧內無元素 2 iload_0 [i] *將本地變數0[i]放入棧頂中*,此時棧內有一個元素,即為0 3 iinc 0 1 [i] *將制定的int型變數[i]增加指定值[1]*,這時i=0+1=1 6 iinc 0 1 [i] *將制定的int型變數[i]增加指定值[1]*,這時i=1+1=2 9 iload_0 [i] *將本地變數0[i]放入棧頂中*,此時棧內有兩個元素,0和2,棧頂為2 10 iadd *將棧頂兩個int類型數值相加*,結果壓入棧頂,此時棧內一個元素為0+2=2 11 istore_1 [y] *將棧頂元素存入本地變數1中*[變數1就是y] 12 getstatic java.lang.System.out : java.io.PrintStream [21] 15 iload_1 [y] 16 invokevirtual java.io.PrintStream.println(int) : void [27] 19 return
??可以看到i++ + ++i的運行結果:遇到i++是先取i(初始i=0)的值(壓入棧),然後進行自加(此時i=1),遇到+號標記繼續(腦補一下逆波蘭運算式,這裡就不說明java的詞法分析、文法分析、語義分析、代碼產生的過程了),遇到++i,先進行自加(此時i=2),然後取i的值(壓入棧),然後將棧頂兩元素相加即可結果。
??假如有個變數m=i++ + i++ + ++i(i初始為0)那麼結果等於多少呢,我們來分析一下。
??初始i=0, 遇到i++,將i的值壓入棧(棧內一個元素:0),自加,此時i=1,遇到+號標記繼續,遇到i++,將i值壓入棧內(棧內元素:1,0),算上之前標記的+號,棧內兩元素相加之後壓入棧(棧內元素:1),i值自加,此時i=2,遇到+號標記繼續,遇到++i,將i值自加,此時i=3壓入棧內(棧內元素3,1),算上之前標記的+號,棧內兩元素相加之後入棧(棧內元素為4),最後將棧頂元素存入本地變數m中,結束。整個相加過程m=0+1+3=4. 到這裡,如果覺得有疑問可以開啟編譯器跑一下m=i++ + i++ + ++i(i初始為0)。
??那麼int z = i++ + ++i + ++i + ++i + i++ + ++i(初始i=0);可以得到的結果為z=0+2+3+4+4+6=19.
??這個例子的講解就此結束。這裡博主不是想要講解一下i++ + ++i之類的問題,而是希望大家可以通過這個問題認識學習反編譯的重要性,能夠更深刻的認識問題。就比如上小學一年級時,考試全是個位元加減,但是基本沒人得滿分,因為那時候個位元加減也是很難滴;後來到了三四年級學到乘除法的時候,個位元加減基本不會算錯了;當你學到高等數學的時候你還會為普通的加減乘除煩惱嚒?會當淩絕頂,一覽眾山小。
??這裡博主準備再將一個例子,加深一下印象,這是前幾天遇到的一個問題,首先看代碼舉例:
package interview;import java.util.HashMap;import java.util.Map;public class JavapTest2{ public static Map m = new HashMap(){ { put("key1","value1"); } };}
??這段代碼就是定義一個靜態類成員變數m,並附初始值。很多朋友應該不太習慣這種用法,一般的就是:
public static Map m = new HashMap();
??要賦值就會繼續m.put(“key1”,”value1”);之類的。
??那麼這段代碼的背後到底是什麼呢?同樣祭出我們的反編譯。
??發現產生了兩個class檔案,分別為JavapTest2.class和JavapTest2$1.class.
JavapTest2.class:
// Compiled from JavapTest2.java (version 1.7 : 51.0, super bit)public class interview.JavapTest2 { // Field descriptor #6 Ljava/util/Map; // Signature: Ljava/util/Map; public static java.util.Map m; // Method descriptor #10 ()V // Stack: 2, Locals: 0 static {}; 0 new interview.JavapTest2$1 [12] 3 dup 4 invokespecial interview.JavapTest2$1() [14] 【博主自加:調用執行個體初始化方法】 7 putstatic interview.JavapTest2.m : java.util.Map [17] 【博主自加:為指定的類的靜態域賦值】 10 return Line numbers: [pc: 0, line: 8] [pc: 10, line: 12] // Method descriptor #10 ()V // Stack: 1, Locals: 1 public JavapTest2(); 0 aload_0 [this] 1 invokespecial java.lang.Object() [21] 4 return Line numbers: [pc: 0, line: 6] Local variable table: [pc: 0, pc: 5] local: this index: 0 type: interview.JavapTest2 Inner classes: [inner class info: #12 interview/JavapTest2$1, outer class info: #0 inner name: #0, accessflags: 0 default]}
JavapTest2$1.class:
// Compiled from JavapTest2.java (version 1.7 : 51.0, super bit)// Signature: Ljava/util/HashMap;class interview.JavapTest2$1 extends java.util.HashMap { // Method descriptor #6 ()V // Stack: 3, Locals: 1 JavapTest2$1(); 0 aload_0 [this] 1 invokespecial java.util.HashMap() [8] 【博主自加:invokespecial是調用父類的建構函式初始化方法】 4 aload_0 [this] 5 ldc [10] 7 ldc [12] 9 invokevirtual interview.JavapTest2$1.put(java.lang.Object, java.lang.Object) : java.lang.Object [14] 【博主自加:調用介面方法】 12 pop 13 return Line numbers: [pc: 0, line: 8] [pc: 4, line: 10] [pc: 13, line: 1] Local variable table: [pc: 0, pc: 14] local: this index: 0 type: new interview.JavapTest2(){} Inner classes: [inner class info: #1 interview/JavapTest2$1, outer class info: #0 inner name: #0, accessflags: 0 default] Enclosing Method: #27 #0 interview/JavapTest2}
??可以看到產生了兩個class檔案,很顯然這裡是內部類的實現,而且是匿名內部類,不然JavapTest2$1.class的1就是其它的類名了。
??這裡博主開始造“坑”了,稍微修改一下代碼,如下(注意內部類中的m.put和put的區別):
package interview;import java.util.HashMap;import java.util.Map;public class JavapTest2{ public static Map m = new HashMap(){ { m.put("key1","value1"); } };}
??這樣,發現編譯器也沒有報錯,但是這樣可不可以呢?在類中加入一個main方法:public static void main(String args[]){}運行一下,報如下錯誤(ExceptionInInitializerError):
Exception in thread "main" java.lang.ExceptionInInitializerErrorCaused by: java.lang.NullPointerException at interview.JavapTest2$1.(JavapTest2.java:10) at interview.JavapTest2.(JavapTest2.java:8)
??Why? 是不是一臉懵逼?反編譯一下,你就知道。JavapTest2.class和之前的沒有變化,有變化的是JavapTest2$1.class,貼出反編譯結果:
// Compiled from JavapTest2.java (version 1.7 : 51.0, super bit)// Signature: Ljava/util/HashMap;class interview.JavapTest2$1 extends java.util.HashMap { // Method descriptor #6 ()V // Stack: 3, Locals: 1 JavapTest2$1(); 0 aload_0 [this] 1 invokespecial java.util.HashMap() [8] 4 getstatic interview.JavapTest2.m : java.util.Map [10] 7 ldc [16] 9 ldc [18] 11 invokeinterface java.util.Map.put(java.lang.Object, java.lang.Object) : java.lang.Object [20] [nargs: 3] 16 pop 17 return Line numbers: [pc: 0, line: 8] [pc: 4, line: 10] [pc: 17, line: 1] Local variable table: [pc: 0, pc: 18] local: this index: 0 type: new interview.JavapTest2(){} Inner classes: [inner class info: #1 interview/JavapTest2$1, outer class info: #0 inner name: #0, accessflags: 0 default] Enclosing Method: #11 #0 interview/JavapTest2}
??上面的第4和11(不是行號,是pc號)與修改之前的第4和9一一對應。
??這裡詳細解釋一下這個運行流程:
??首先JavapTest2的程式入口是main方法,這個方法什麼事都沒幹,但是這裡已經觸發了對JavaTest2的類的執行個體化(就是上面異常中的),那麼啟動並執行是這段:
static {}; 0 new interview.JavapTest2$1 [12] 3 dup 4 invokespecial interview.JavapTest2$1() [14] 7 putstatic interview.JavapTest2.m : java.util.Map [17] 10 return
??這段指令是首先是new JavaTest2$1這個匿名內部類,然後dup(將當前棧頂元素複製一份,並壓入棧中),然後調用匿名內部類的建構函式,直到這雷根本沒有interview.JavapTest2.m的什麼事,所以執行到這一步還沒有m什麼鳥事。
?interview.JavapTest2.m此時為null. 因為m為static類型,在類載入之後的準備階段會為類變數分配記憶體並設定類變數初始值,這些變數所使用的記憶體都將在方法區中進行分配。這時候進行記憶體配置的僅包括類變數(static修飾的變數),而不包括執行個體變數,執行個體變數將會在對象執行個體化時隨著對象一起分配在java堆中。這裡所說的初始值“通常情況”下是資料類型的零值,假設一個類變數的定義為:
?public static int value = 123;
?那變數value在準備階段過後的初始值為0而不是123,因為這時候尚未開始執行java方法,而把value賦值為123的putstatic指令是程式被編譯後,存放於類構造器()方法之中,所以把value賦值為123的動作將在初始化階段才會執行。
?這裡的m是參考型別,參考型別的零值是null.
??接下去執行匿名內部的執行個體化(就是上面異常的),如下:
JavapTest2$1(); 0 aload_0 [this] 1 invokespecial java.util.HashMap() [8] 4 getstatic interview.JavapTest2.m : java.util.Map [10] 7 ldc [16] 9 ldc [18] 11 invokeinterface java.util.Map.put(java.lang.Object, java.lang.Object) : java.lang.Object [20] [nargs: 3] 16 pop 17 return
??注意到第4條getstatic interview.JavapTest2.m : java.util.Map [10]這裡的getstatic是指擷取指定類的靜態域,但是這個m此時還是null,所以是java.lang.NullPointerException,所以這段代碼會報錯。
附:ExceptionInInitializerError在JVM規範中這樣定義:
1. 如果JVM試圖建立類ExceptionInInitializerError的新執行個體,但是因為出現OOM而無法建立新執行個體,那麼就拋出OOM作為代替;
2. 如果初始化器拋出一些Exception,而且Exception類不是Error或者它的某個子類,那麼就會建立ExceptionInInitializerError類的一個新執行個體,並用Exception作為參數,用這個執行個體代替Exception.