本文涉及到一些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开发者很容易于不知不觉中养成了对技术不求甚解的“恶习”,因为既然代码能够正常工作,那又何必费心地去追根问底?但话又说回来,如果仅满足于知其然,又怎会在技术上有所进步和提高?等到年纪一大,就被年轻人给淘汰了。而这种现象的出现,到底应该怪微软,怪周遭的环境,还是自己呢?

【转载】C#之int与Java之Integer的区别的更多相关文章

  1. JSONObject转换Int类型--java.lang.Integer cannot be cast to java.lang.String

    参数 params={"abc":0} JSONObject转换Int类型 JSONObject json = JSONObject.fromObject(params); if ...

  2. [转载]java int与integer的区别

    声明: 本篇文章属于转载文章,来源:

  3. java中int和Integer的区别

    Integer与int的种种比较你知道多少?  转载自http://www.cnblogs.com/liuling/archive/2013/05/05/intAndInteger.html 如果面试 ...

  4. java int与integer的区别

    int与integer的区别从大的方面来说就是基本数据类型与其包装类的区别: int 是基本类型,直接存数值,而integer是对象,用一个引用指向这个对象 1.Java 中的数据类型分为基本数据类型 ...

  5. 转 java int与Integer的区别

    int是java提供的8种原始数据类型之一, Java为每个原始类型提供了封装类,Integer是java为int提供的封装类. int 是基本类型,Integer是引用类型. java.lang.I ...

  6. 【转】java int与integer的区别

    java int与integer的区别 int与integer的区别从大的方面来说就是基本数据类型与其包装类的区别: int 是基本类型,直接存数值,而integer是对象,用一个引用指向这个对象 1 ...

  7. Java中 int和Integer的区别+包装类

    --今天用Integer 和Integer 比较 发现有问题,于是去查了查. 1.Java 中的数据类型分为基本数据类型和引用数据类型 int是基本数据类型,Integer是引用数据类型: Inget ...

  8. java int和Integer的区别

    今天偶然和同学讨论到int和Integer的区别是,发现自己对这个问题了解的并不是很清楚,而且有些概念还是错的,所以在这对int和Integer的区别做一个总结. int与integer的区别从大的方 ...

  9. java面试题之int和Integer的区别

    int和Integer的区别 1.Integer是int的包装类,int则是java的一种基本数据类型 2.Integer变量必须实例化后才能使用,而int变量不需要 3.Integer实际是对象的引 ...

随机推荐

  1. Origin的图片导出问题

    很多会议投稿都会要求提交的pdf文件用的是type1字体,因为type1字体是矢量字体,无论怎么放大缩小都不会失真.一旦pdf里嵌入了其他非矢量字体,例如type3字体,就会通不过测试,一个典型的例子 ...

  2. fgets函数执行完成后,文件指针如何移动?

    用fgets执行之后,读取了文件中的一行,这时文件位置指针是自动指向文件的下一行的开头吗,还是指向当前行的结尾?如果一行的字符串没读取完会怎样? 实例结果: 如果一行的字符串没读取完会,下一次会接着上 ...

  3. Java程序员的日常 —— 《编程思想》包和访问权限

    包的作用 在C++中有命名空间的概念,这是因为,不同的项目或者人写出来的代码,可能名称是一样的.比如,java.util中有List,java.awt中也有List,那么我们在开发中,怎么会知道使用的 ...

  4. [jQuery学习系列四 ]4-Jquery学习四-事件操作

    前言:今天看知乎偶然看到中国有哪些类似于TED的节目, 回答中的一些推荐我给记录下来了, 顺便也在这里贴一下: 一席 云集 听道 推酷 青年中国说 SELF格致论道 参考:http://www.365 ...

  5. 解决pageControl页面设置无效问题

    废话不多说,先上代码 1.添加pageViewControl - (void)addPageControl { UIPageControl *pageControl = [[UIPageControl ...

  6. arcgis server账号需要设置地图缓存的访问权限

    如果gis服务起不来,那么可以看看arcgis server账号有没有设置地图缓存的访问权限,有可能arcgis server账号没有这个文件夹的访问权限.  本文用菊子曰发布

  7. 谈一谈PHP的代码重构

    随着 PHP 从一种简单的脚本语言转变为一种成熟的编程语言,一个典型的 PHP 应用程序的代码库的复杂性也随之增大.为了控制对这些应用程序的支持和维护,我们可以使用各种测试工具来自动化该流程.其中一种 ...

  8. Win7下VS2010使用“ASP.Net 3.5 Claims-aware Template”创建ClaimsAwareWebSite报"HRESULT: 0x80041FEB"错误的解决办法

    问题描述: 使用VS2010的WIF开发模板创建“Claims-aware ASP.NET Site”.“Claims-aware WCF Service”,下载安装后,创建网站时,报错"H ...

  9. php中如何创建文件夹

    这个功能比较简单,直接上代码了: $dir = iconv("UTF-8", "GBK", "Public/bookcover"); if ...

  10. Bag标签之中的一个行代码实行中文分词实例2

    例1: 分词(返回以逗号隔开每一个词带上引號的词组.gap=",",quotes="'"或quotes='"') 单引號 <bag id=pPa ...