【转载】C#之int与Java之Integer的区别
本⽂涉及到⼀些JVM原理和Java的字节码指令,推荐感兴趣的读者阅读⼀本有关JVM的经典书籍《深⼊Java(第2版)》,将它与我在
《.NET 4.0⾯向对象编程漫谈》中介绍的CLR原理与IL汇编指令作个对⽐,相信读者会有⼀定的启发。⽽仔细对⽐两个类似事物的异同,是很有效的学习⽅法之⼀。
1 奇特的程序输出
前段时间,⼀个学⽣给我看了⼀段“⾮常诡异”的Java代码:
public class Integer {
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[];
//……
常用的java编译器有哪些}
再仔细看看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开发者很容易于不知不觉中养成了对技术不求甚解的“恶习”,因为既然代码能够正常⼯作,那⼜何必费⼼地去追根问底?但话⼜说回来,如果仅满⾜于知其然,⼜怎会在技术上有所进步和提⾼?等到年纪⼀⼤,就被年轻⼈给淘汰了。⽽这种现象的出现,到底应该怪微软,怪周遭
的环境,还是⾃⼰呢?
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论