本文涉及到一些JVM原理和Java的字節(jié)碼指令,推薦感興趣的讀者閱讀一本有關(guān)JVM的經(jīng)典書籍《深入Java虛擬機(jī)(第2版)》,將它與我在《.NET 4.0面向?qū)ο缶幊搪劇分薪榻B的CLR原理與IL匯編指令作個(gè)對(duì)比,相信讀者會(huì)有一定的啟發(fā)。而仔細(xì)對(duì)比兩個(gè)類似事物的異同,是很有效的學(xué)習(xí)方法之一。
1 奇特的程序輸出
前段時(shí)間,一個(gè)學(xué)生給我看了一段“非常詭異”的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
}
}
讓這個(gè)學(xué)生最困惑的是,為什么這些如此相似的代碼會(huì)有這樣令人意外的輸出?
我平時(shí)多使用C#,Java用得不多,初看到這段代碼的輸出,我也同樣非常奇怪:怎么會(huì)這樣呢?100和200這兩個(gè)整型數(shù)值對(duì)Integer這個(gè)類有本質(zhì)上的差別嗎?
為了弄明白出現(xiàn)上述現(xiàn)象的底層原因,我使用javap工具反匯編了Java編譯器生成的.class文件:
通過仔細(xì)閱讀Java編譯器生的字節(jié)碼,我發(fā)現(xiàn)以下給Integer變量賦值的語句:
Integer v1=100;
實(shí)際上調(diào)用的是Integer.valueOf方法。
而完成兩個(gè)Integer變量比較的以下語句:
System.Console.WriteLine(v1 == v2);
實(shí)際生成的是if_acmpne指令。其中的a代表“address”,cmp代表“Compare”,ne代表“not equal”。
這條指令的含義是:比較Java方法棧中的兩個(gè)操作數(shù)(即v1與v2),看看它們是不是指向堆中的同一個(gè)對(duì)象。
當(dāng)給v1和v2賦值100時(shí),它們將引用同一個(gè)Integer對(duì)象。
那為什么當(dāng)值改為200時(shí),w1和w2就“翻臉了”,分別引用不同的Integer對(duì)象?
秘密就在于Integer.valueOf方法。幸運(yùn)的是,Java的類庫是開源的,所以我們可以毫不費(fèi)力地看到相關(guān)的源代碼:
public static Integer valueOf(int i) {
if(i >= -128 && i <= IntegerCache.high)
return IntegerCache.cache[i + 128];
else
return new Integer(i);
}
一切真相大白,原來Integer在內(nèi)部使用了一個(gè)私有的靜態(tài)類IntegerCache,此類內(nèi)部封裝了一個(gè)Integer對(duì)象的cache數(shù)組來緩存Integer對(duì)象,其代碼如下:
private static class IntegerCache {
static final Integer cache[];
//……
}
再仔細(xì)看看IntegerCache內(nèi)部的代碼,會(huì)看到它使用靜態(tài)初始化塊在cache數(shù)組中保存了[-128,127]區(qū)間內(nèi)的一共256個(gè)Integer對(duì)象。
當(dāng)給Integer變量直接賦整數(shù)值時(shí),如果這個(gè)數(shù)值位于[-128,127]內(nèi),JVM(Java Virtual Machine)就直接使用cache中緩存的Integer對(duì)象,否則,JVM會(huì)重新創(chuàng)建一個(gè)Integer對(duì)象。
一切真相大白。
2 進(jìn)一步探索Integer
我們?cè)龠M(jìn)一步地看看這個(gè)有趣的Integer:
Integer v1 = 500;
Integer v2 = 300;
Integer addResult = v1 + v2; //結(jié)果:800
double divResult = (double)v1/v2; //結(jié)果:1.6666666666666667
喲,居然Integer對(duì)象支持加減乘除運(yùn)算耶!它是怎么做到的?
再次使用javap反匯編.class文件,不難發(fā)現(xiàn):
Integer類的內(nèi)部有一個(gè)私有int類型的字段value,它代表了Integer對(duì)象所“封裝”的整數(shù)值。
private final int value;
當(dāng)需要執(zhí)行v1+v2時(shí),JVM會(huì)調(diào)用v1和v2兩個(gè)Integer對(duì)象的intValue方法取出其內(nèi)部所封裝的整數(shù)值value,然后調(diào)用JVM直接支持的iadd指令將這兩個(gè)整數(shù)直接相加,結(jié)果送回方法棧中,然后調(diào)用Integer.valueOf方法轉(zhuǎn)換為Integer對(duì)象,讓addResult變量引用這一對(duì)象。
除法則復(fù)雜一點(diǎn),JVM先調(diào)用i2d指令將int轉(zhuǎn)換為double,然后再調(diào)用ddiv指令完成浮點(diǎn)數(shù)相除的工作。
通過上述分析,我們可以知道,其實(shí)Integer類本身并不支持加減乘除,而是由Java編譯器將這些加減乘除的語句轉(zhuǎn)換為JVM可以直接執(zhí)行的字節(jié)碼指令(比如本例中用到的iadd和ddiv),其中會(huì)添加許多條用于類型轉(zhuǎn)換的語句。
由此可見,與原始數(shù)據(jù)類型int相比,使用Integer對(duì)象直接進(jìn)行加減乘除會(huì)帶來較低的運(yùn)行性能,應(yīng)避免使用。
3 JDK中Integer類的“彎彎繞”設(shè)計(jì)方案
現(xiàn)在,我們站在一個(gè)更高的角度,探討一下Integer的設(shè)計(jì)。
我個(gè)人認(rèn)為,給Integer類型添加一個(gè)“對(duì)象緩沖”不是一個(gè)好的設(shè)計(jì),從最前面的示例代碼大家一定會(huì)感到這一設(shè)計(jì)給應(yīng)用層的代碼帶來了一定的混亂。另外,我們看到JDK設(shè)計(jì)者只緩存了[-128,127]共256個(gè)Integer對(duì)象,他可能認(rèn)為這個(gè)區(qū)間內(nèi)的整數(shù)是最常用的,所以應(yīng)該緩存以提升性能。就我來看,這未免有點(diǎn)過于“自以為是”了,說這個(gè)區(qū)間內(nèi)的Integer對(duì)象用得最多有什么依據(jù)?對(duì)于那些經(jīng)常處理>128的整數(shù)值的應(yīng)用程序而言,這個(gè)緩存一點(diǎn)用處也沒有,是個(gè)累贅。就算真要緩存,那也最好由應(yīng)用程序開發(fā)者自己來實(shí)現(xiàn),因?yàn)樗梢砸罁?jù)自己開發(fā)的實(shí)際情況緩存真正用到的對(duì)象,而不需背著這個(gè)包容著256個(gè)Integer對(duì)象的大包袱。
而且前面也看到了,基于Integer對(duì)象的加減乘除會(huì)增加許多不必要的類型轉(zhuǎn)換指令,遠(yuǎn)不如直接使用原始數(shù)據(jù)類型更快捷更可靠。
其實(shí)上用得最多的不是Integer對(duì)象而是它所封裝的一堆靜態(tài)方法(這些方法提供了諸如類型轉(zhuǎn)換等常用功能),我很懷疑在實(shí)際開發(fā)中有多少場(chǎng)合需要去創(chuàng)建大量的Integer對(duì)象,而且還假設(shè)它們封裝的數(shù)值還位于[-128,127]區(qū)間之內(nèi)?
緩存Integer對(duì)象還對(duì)多線程應(yīng)用程序帶來了一定的風(fēng)險(xiǎn),因?yàn)榭赡軙?huì)有多個(gè)線程同時(shí)存取同一個(gè)緩存了的Integer對(duì)象。不過JDK設(shè)計(jì)者已經(jīng)考慮到了這個(gè)問題,我看到Integer類的字段都是final的,不可改,是一個(gè)不可變類,所以可以在多線程環(huán)境下安全地訪問。盡管在使用上沒問題,但這一切是不是有點(diǎn)彎彎繞?去掉這個(gè)對(duì)象緩存,Integer類型是不是“更輕爽”“更好用”?
4 C# int挑戰(zhàn)Java Integer
將Java的設(shè)計(jì)與.NET(以C#為例)的設(shè)計(jì)作個(gè)比較是有趣的。
Java將數(shù)據(jù)類型分為“原始數(shù)據(jù)類型”和“引用數(shù)據(jù)類型”兩大類,int是原始數(shù)據(jù)類型,為了向開發(fā)者提供一些常用的功能(比如將String轉(zhuǎn)換為int),所以JDK提供了一個(gè)引用類型Integer,封裝這些功能。
.NET則不一樣,它的數(shù)據(jù)類型分為“值類型”和“引用數(shù)據(jù)類型”兩大類,int屬于值類型,本身就擁有豐富的方法,請(qǐng)看以下C#代碼:
int i = 100;
string str = i.ToString(); //int變量本身就擁有“一堆”的方法
使用.NET的反匯編器ildasm查看一下上述代碼生成的IL指令,不難發(fā)現(xiàn)C#編譯器會(huì)將int類型映射為System.Int32結(jié)構(gòu):
注意System.Int32是一個(gè)值類型,生存于線程堆棧中,一般來說,在多線程環(huán)境下,使用值類型的變量往往比引用類型的變量更安全,因?yàn)樗鼫p少了多線程訪問同一對(duì)象所帶來的問題。
=================================
簡(jiǎn)要解釋一下:請(qǐng)對(duì)比以下兩個(gè)方法:
void DoSomethingWithValueType(int value);
void DoSomethingWithReferenceType(MyClass obj);
當(dāng)多個(gè)線程同時(shí)執(zhí)行上述兩個(gè)方法時(shí),線程函數(shù)使用值類型的參數(shù)value是比較安全的,不用擔(dān)心多個(gè)線程互相影響,但引用類型的obj參數(shù)就要小心了,如果多個(gè)線程接收到的obj參數(shù)有可能引用同一個(gè)MyClass對(duì)象,為保證運(yùn)行結(jié)果的正確,有可能需要給此對(duì)象加鎖。
====================================
與JVM一樣,.NET的CLR也提供了add等專用指令完成加減乘除功能。
從開發(fā)者使用角度而言,C#的int既具有與Java的原始數(shù)據(jù)類型int一樣的在虛擬機(jī)級(jí)別的專用指令,又具有Java包裝類Integer所擁有的一些功能,還同時(shí)避免了Java中Integer的那種比較古怪的特性,個(gè)人認(rèn)為,C#中的int比Java中的int/Integer更好用,更易用。
但從探索技術(shù)內(nèi)幕而言則大不一樣,Java使用Integer一個(gè)類就“搞定”了所有常用的整數(shù)處理功能,而對(duì)于.NET的System.In32結(jié)構(gòu),好奇的朋友不妨用Reflector去查看一下相關(guān)的源碼,會(huì)發(fā)現(xiàn)System.Int32在內(nèi)部許多地方使用了Number類所封裝的功能,還用到了NumberFormatInfo(提供數(shù)字的格式化信息)、CultureInfo(提供當(dāng)前文化信息)等相關(guān)類型,如果再算加上一堆的接口,那真是“相當(dāng)?shù)亍睆?fù)雜。
比對(duì)一下Java平臺(tái)與.NET平臺(tái),往往會(huì)發(fā)現(xiàn)在許多地方Java封裝得較少。
從應(yīng)用程序開發(fā)角度來看,不少地方Java在使用上不如.NET方便。就拿本文所涉及的非常常見的整數(shù)類型及其運(yùn)算而言,相信大家都看到了,使用Java編程需要留心這個(gè)“Intege對(duì)象緩存”的陷阱,而.NET則很貼心地把這些已發(fā)現(xiàn)的陷阱(.NET設(shè)計(jì)者說:當(dāng)然肯定會(huì)有沒發(fā)現(xiàn)的陷阱,但那就不關(guān)我事了)都蓋上了“厚厚”的井蓋,讓開發(fā)者很省心,因而帶來了較高的開發(fā)效率和較好的開發(fā)體驗(yàn)。
但另一方面,Java的JDK代碼一覽無余,是開放的,你要探索其技術(shù)內(nèi)幕,總是很方便,這點(diǎn)還是比較讓人放心。
.NET則相對(duì)比較封閉,總是遮遮掩掩,想一覽其廬山真相還真不容易,而且我感覺它為開發(fā)者考慮得太周到了,服務(wù)得太好了,這不見得是一件好事。因?yàn)槿诵缘娜觞c(diǎn)之一就是“好逸惡勞”,生活太舒服了,進(jìn)取精神就會(huì)少掉不少,.NET開發(fā)者很容易于不知不覺中養(yǎng)成了對(duì)技術(shù)不求甚解的“惡習(xí)”,因?yàn)榧热淮a能夠正常工作,那又何必費(fèi)心地去追根問底?但話又說回來,如果僅滿足于知其然,又怎會(huì)在技術(shù)上有所進(jìn)步和提高?等到年紀(jì)一大,就被年輕人給淘汰了。而這種現(xiàn)象的出現(xiàn),到底應(yīng)該怪微軟,怪周遭的環(huán)境,還是自己呢?