【轉載】C#之int與Java之Integer的區別

xyzhuzhou發表於2016-05-23

本文涉及到一些JVM原理和Java的位元組碼指令,推薦感興趣的讀者閱讀一本有關JVM的經典書籍《深入Java虛擬機器(第2版)》,將它與我在《.NET 4.0物件導向程式設計漫談》中介紹的CLR原理與IL彙編指令作個對比,相信讀者會有一定的啟發。而仔細對比兩個類似事物的異同,是很有效的學習方法之一。

1 奇特的程式輸出

          前段時間,一個學生給我看了一段“非常詭異”的Java程式碼:
   public class testInteger {
    public static void main(String[] args){
        Integer v1=100;
        Integer v2=100;
        System.out.println(v1==v2); //輸出:true
        Integer w1=200;
        Integer w2=200;
        System.out.println(w1==w2); //輸出:false
    }
}

          讓這個學生最困惑的是,為什麼這些如此相似的程式碼會有這樣令人意外的輸出?

         我平時多使用C#,Java用得不多,初看到這段程式碼的輸出,我也同樣非常奇怪:怎麼會這樣呢?100和200這兩個整型數值對Integer這個類有本質上的差別嗎?

         為了弄明白出現上述現象的底層原因,我使用javap工具反彙編了Java編譯器生成的.class檔案:

   

          通過仔細閱讀Java編譯器生的位元組碼,我發現以下給Integer變數賦值的語句:

         Integer v1=100;

          實際上呼叫的是Integer.valueOf方法。

           而完成兩個Integer變數比較的以下語句:

          System.Console.WriteLine(v1 == v2);

          實際生成的是if_acmpne指令。其中的a代表“address”,cmp代表“Compare”,ne代表“not equal”。

         這條指令的含義是:比較Java方法棧中的兩個運算元(即v1與v2),看看它們是不是指向堆中的同一個物件。

         當給v1和v2賦值100時,它們將引用同一個Integer物件。

         那為什麼當值改為200時,w1和w2就“翻臉了”,分別引用不同的Integer物件?

         祕密就在於Integer.valueOf方法。幸運的是,Java的類庫是開源的,所以我們可以毫不費力地看到相關的原始碼:

 public static Integer valueOf(int i) {
        if(i >= -128 && i <= IntegerCache.high)
            return IntegerCache.cache[i + 128];
        else
            return new Integer(i);
    }

        一切真相大白,原來Integer在內部使用了一個私有的靜態類IntegerCache,此類內部封裝了一個Integer物件的cache陣列來快取Integer物件,其程式碼如下:

 private static class IntegerCache {
      static final Integer cache[];
      //……
}

         再仔細看看IntegerCache內部的程式碼,會看到它使用靜態初始化塊在cache陣列中儲存了[-128,127]區間內的一共256個Integer物件。

         當給Integer變數直接賦整數值時,如果這個數值位於[-128,127]內,JVM(Java Virtual Machine)就直接使用cache中快取的Integer物件,否則,JVM會重新建立一個Integer物件。

         一切真相大白。

 2 進一步探索Integer

     我們再進一步地看看這個有趣的Integer:

     Integer v1 = 500;
    Integer v2 = 300;
    Integer addResult = v1 + v2;   //結果:800
    double divResult = (double)v1/v2;   //結果:1.6666666666666667

         喲,居然Integer物件支援加減乘除運算耶!它是怎麼做到的?

         再次使用javap反彙編.class檔案,不難發現:

        Integer類的內部有一個私有int型別的欄位value,它代表了Integer物件所“封裝”的整數值。

          private final int value;

          當需要執行v1+v2時,JVM會呼叫v1和v2兩個Integer物件的intValue方法取出其內部所封裝的整數值value,然後呼叫JVM直接支援的iadd指令將這兩個整數直接相加,結果送回方法棧中,然後呼叫Integer.valueOf方法轉換為Integer物件,讓addResult變數引用這一物件。

         除法則複雜一點,JVM先呼叫i2d指令將int轉換為double,然後再呼叫ddiv指令完成浮點數相除的工作。

         通過上述分析,我們可以知道,其實Integer類本身並不支援加減乘除,而是由Java編譯器將這些加減乘除的語句轉換為JVM可以直接執行的位元組碼指令(比如本例中用到的iadd和ddiv),其中會新增許多條用於型別轉換的語句。

         由此可見,與原始資料型別int相比,使用Integer物件直接進行加減乘除會帶來較低的執行效能,應避免使用。

 3 JDK中Integer類的“彎彎繞”設計方案

          現在,我們站在一個更高的角度,探討一下Integer的設計。

         我個人認為,給Integer型別新增一個“物件緩衝”不是一個好的設計,從最前面的示例程式碼大家一定會感到這一設計給應用層的程式碼帶來了一定的混亂。另外,我們看到JDK設計者只快取了[-128,127]共256個Integer物件,他可能認為這個區間內的整數是最常用的,所以應該快取以提升效能。就我來看,這未免有點過於“自以為是”了,說這個區間內的Integer物件用得最多有什麼依據?對於那些經常處理>128的整數值的應用程式而言,這個快取一點用處也沒有,是個累贅。就算真要快取,那也最好由應用程式開發者自己來實現,因為他可以依據自己開發的實際情況快取真正用到的物件,而不需揹著這個包容著256個Integer物件的大包袱。

         而且前面也看到了,基於Integer物件的加減乘除會增加許多不必要的型別轉換指令,遠不如直接使用原始資料型別更快捷更可靠。

         其實上用得最多的不是Integer物件而是它所封裝的一堆靜態方法(這些方法提供了諸如型別轉換等常用功能),我很懷疑在實際開發中有多少場合需要去建立大量的Integer物件,而且還假設它們封裝的數值還位於[-128,127]區間之內?

       快取Integer物件還對多執行緒應用程式帶來了一定的風險,因為可能會有多個執行緒同時存取同一個快取了的Integer物件。不過JDK設計者已經考慮到了這個問題,我看到Integer類的欄位都是final的,不可改,是一個不可變類,所以可以在多執行緒環境下安全地訪問。儘管在使用上沒問題,但這一切是不是有點彎彎繞?去掉這個物件快取,Integer型別是不是“更輕爽”“更好用”?

 4  C# int挑戰Java Integer

          將Java的設計與.NET(以C#為例)的設計作個比較是有趣的。

         Java將資料型別分為“原始資料型別”和“引用資料型別”兩大類,int是原始資料型別,為了向開發者提供一些常用的功能(比如將String轉換為int),所以JDK提供了一個引用型別Integer,封裝這些功能。

         .NET則不一樣,它的資料型別分為“值型別”和“引用資料型別”兩大類,int屬於值型別,本身就擁有豐富的方法,請看以下C#程式碼:

     int i = 100;
    string str = i.ToString(); //int變數本身就擁有“一堆”的方法

          使用.NET的反彙編器ildasm檢視一下上述程式碼生成的IL指令,不難發現C#編譯器會將int型別對映為System.Int32結構:

  

         注意System.Int32是一個值型別,生存於執行緒堆疊中,一般來說,在多執行緒環境下,使用值型別的變數往往比引用型別的變數更安全,因為它減少了多執行緒訪問同一物件所帶來的問題。

 =================================

         簡要解釋一下:請對比以下兩個方法:

          void DoSomethingWithValueType(int value);

          void DoSomethingWithReferenceType(MyClass obj);

         當多個執行緒同時執行上述兩個方法時,執行緒函式使用值型別的引數value是比較安全的,不用擔心多個執行緒互相影響,但引用型別的obj引數就要小心了,如果多個執行緒接收到的obj引數有可能引用同一個MyClass物件,為保證執行結果的正確,有可能需要給此物件加鎖。

====================================       

         與JVM一樣,.NET的CLR也提供了add等專用指令完成加減乘除功能。

         從開發者使用角度而言,C#的int既具有與Java的原始資料型別int一樣的在虛擬機器級別的專用指令,又具有Java包裝類Integer所擁有的一些功能,還同時避免了Java中Integer的那種比較古怪的特性,個人認為,C#中的int比Java中的int/Integer更好用,更易用。

         但從探索技術內幕而言則大不一樣,Java使用Integer一個類就“搞定”了所有常用的整數處理功能,而對於.NET的System.In32結構,好奇的朋友不妨用Reflector去檢視一下相關的原始碼,會發現System.Int32在內部許多地方使用了Number類所封裝的功能,還用到了NumberFormatInfo(提供數字的格式化資訊)、CultureInfo(提供當前文化資訊)等相關型別,如果再算加上一堆的介面,那真是“相當地”複雜。

         比對一下Java平臺與.NET平臺,往往會發現在許多地方Java封裝得較少。

         從應用程式開發角度來看,不少地方Java在使用上不如.NET方便。就拿本文所涉及的非常常見的整數型別及其運算而言,相信大家都看到了,使用Java程式設計需要留心這個“Intege物件快取”的陷阱,而.NET則很貼心地把這些已發現的陷阱(.NET設計者說:當然肯定會有沒發現的陷阱,但那就不關我事了)都蓋上了“厚厚”的井蓋,讓開發者很省心,因而帶來了較高的開發效率和較好的開發體驗。

         但另一方面,Java的JDK程式碼一覽無餘,是開放的,你要探索其技術內幕,總是很方便,這點還是比較讓人放心。

        .NET則相對比較封閉,總是遮遮掩掩,想一覽其廬山真相還真不容易,而且我感覺它為開發者考慮得太周到了,服務得太好了,這不見得是一件好事。因為人性的弱點之一就是“好逸惡勞”,生活太舒服了,進取精神就會少掉不少,.NET開發者很容易於不知不覺中養成了對技術不求甚解的“惡習”,因為既然程式碼能夠正常工作,那又何必費心地去追根問底?但話又說回來,如果僅滿足於知其然,又怎會在技術上有所進步和提高?等到年紀一大,就被年輕人給淘汰了。而這種現象的出現,到底應該怪微軟,怪周遭的環境,還是自己呢?

相關文章