程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> JAVA綜合教程 >> 通過Java反編譯揭開一些問題的真相

通過Java反編譯揭開一些問題的真相

編輯:JAVA綜合教程

通過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.ExceptionInInitializerError
Caused 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.

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved