Java的自动拆箱和装箱是Java语言的一颗语法糖

来源:互联网 发布:js php 加密 编辑:程序博客网 时间:2024/05/22 12:15



在本文中,笔者向大家介绍下Java中一个非常重要也非常有趣的特性,就是自动装箱与拆箱,并从源码中解读自动装箱与拆箱的原理,同时这种特性也留有一个陷阱。开发者如果不注意,就会很容易跌入这个陷阱。

装箱(Autoboxing)

          大家在平时编写Java程序时,都常常以以下方式来定义一个Integer对象:

[java] view plain copy
  1. Integer i=100;  

          从上面的代码中,大家可以得知,i为一个Integer类型的引用,100为Java中的基础数据类型(primitive data type)。而这种直接将一个基础数据类型传给其相应的封装类(wrapper class)的做法,便是自动装箱(Autoboxing)。

          在jdk 1.5中,自动装箱首次被引入。而在jdk 1.5之前,如果你想要定义一个value为100的Integer对象,则需要这样做:

[java] view plain copy
  1. Integer i=new Integer (100);  

原理

          我们在以上代码“Integer i=100;”处打一个断点,跟踪一下。

           

          接下来,我们可以看到,程序跳转到了Integer类的valueOf(int i)方法中

[java] view plain copy
  1. /** 
  2.      * Returns a <tt>Integer</tt> instance representing the specified 
  3.      * <tt>int</tt> value. 
  4.      * If a new <tt>Integer</tt> instance is not required, this method 
  5.      * should generally be used in preference to the constructor 
  6.      * {@link #Integer(int)}, as this method is likely to yield 
  7.      * significantly better space and time performance by caching 
  8.      * frequently requested values. 
  9.      * 
  10.      * @param  i an <code>int</code> value. 
  11.      * @return a <tt>Integer</tt> instance representing <tt>i</tt>. 
  12.      * @since  1.5 
  13.      */  
  14.     public static Integer valueOf(int i) {  
  15.         if(i >= -128 && i <= IntegerCache.high)  
  16.             return IntegerCache.cache[i + 128];  
  17.         else  
  18.             return new Integer(i);  
  19.     }  

          换句话说,装箱就是jdk自己帮你完成了调用Integer.valueOf(100)。

拆箱(Unboxing)

[java] view plain copy
  1. Integer integer100=100;  
  2. int int100=integer100;  

          从上面的代码中,大家可看出integer100为一个Integer类型的引用,int100为一个int类型的原始数据类型。但是,我们可以将一个Integer类型的对象赋值给其相应原始数据类型的变量。这便是拆箱。

          拆箱与装箱是相反的操作。装箱是将一个原始数据类型赋值给相应封装类的变量。而拆箱则是将一个封装类的变量赋值给相应原始数据类型的变量。装箱、拆箱的名字也取得相当贴切。

原理

          笔者相信大家也都猜到了,拆箱过程中jdk为我们做了什么。我们还是通过实验来证明我们的猜想吧。

          在以上代码的第二行代码打上断点,即在“int int100=integer100;”上打上断点,跟踪一下。

          我们可以看到,程序跳转到了Integer的intValue()方法。

[java] view plain copy
  1. /** 
  2.      * Returns the value of this <code>Integer</code> as an 
  3.      * <code>int</code>. 
  4.      */  
  5.     public int intValue() {  
  6.     return value;  
  7.     }  

          也就是,jdk帮我们完成了对intValue()方法的调用。对于以上的实验而言,便是调用integer100的intValue()方法,将其返回值赋给了int100。

实验1

[java] view plain copy
  1. Integer integer400=400;  
  2. int int400=400;  
  3. System.out.println(integer400==int400);  

          在以上代码的第三行中,integer400与int400执行了==运行。而这两个是不同类型的变量,到底是integer400拆箱了,还是int400装箱了呢?运行结果是什么呢?

          ==运算是判断两个对象的地址是否相等或者判断两个基础数据类型的值是否相等。所以,大家很容易推测到,如果integer400拆箱了,则说明对比的是两个基础类型的值,那此时必然相等,运行结果为true;如果int400装箱了,则说明对比的是两个对象的地址是否相等,那此时地址必然不相等,运行结果为false。(至于为什么笔者对它们赋值为400,就是后面将要讲到的陷阱有关)。

          我们实际的运行结果为true。所以是integer400拆箱了。对代码跟踪的结果也证明这一点。

实验2

[java] view plain copy
  1. Integer integer100=100;  
  2. int int100=100;  
  3. System.out.println(integer100.equals(int100));  

          在以上代码的第三行中,integer100的方法equals的参数为int100。我们知道equals方法的参数为Object,而不是基础数据类型,因而在这里必然是int100装箱了。对代码跟踪的结果也证明了这一点。

          其实,如果一个方法中参数类型为原始数据类型,所传入的参数类型为其封装类,则会自动对其进行拆箱;相应地,如果一个方法中参数类型为封装类型,所传入的参数类型为其原始数据类型,则会自动对其进行装箱。

实验3

[java] view plain copy
  1. Integer integer100 = 100;  
  2. int int100 = 100;  
  3. Long long200 = 200l;  
  4. System.out.println(integer100 + int100);  
  5. System.out.println(long200 == (integer100 + int100));  
  6. System.out.println(long200.equals(integer100 + int100));  

          在第一个实验中,我们已经得知,当一个基础数据类型与封装类进行==运算时,会将封装类进行拆箱。那如果+、-、*、/呢?我们在这个实验中,就可知道。

          如果+运算,会将基础数据类型装箱,那么:

  • 第4行中,integer100+int100就会得到一个类型为Integer且value为200的对象o,并执行这个对象的toString()方法,并输出”200”;
  • 第5行中,integer100+int100就会得到一个类型为Integer且value为200的对象o,==运算将这个对象与long200对象进行对比,显然,将会输出false;
  • 第6行中,integer100+int100就会得到一个类型为Integer且value为200的对象o,Long的equals方法将long200与o对比,因为两都是不同类型的封装类,因而输出false;

          如果+运算,会将封装类进行拆箱,那么:

  • 第4行中,integer100+int100就会得到一个类型为int且value为200的基础数据类型b,再将b进行装箱得到o,执行这个对象的toString()方法,并输出”200”;
  • 第5行中,integer100+int100就会得到一个类型为int且value为200的基础数据类型b1,==运算将long200进行拆箱得到b2,显然b1==b2,输出true;
  • 第6行中,integer100+int100就会得到一个类型为int且value为200的基础数据类型b,Long的equals方法将b进行装箱,但装箱所得到的是类型为Integer的对象o,因为o与long200为不同的类型的对象,所以输出false;

          程序运行的结果为:      

[java] view plain copy
  1. 200  
  2. true  
  3. false  

          因而,第二种推测是正确,即在+运算时,会将封装类进行拆箱。

陷阱

陷阱1

[java] view plain copy
  1. Integer integer100=null;  
  2. int int100=integer100;  

          这两行代码是完全合法的,完全能够通过编译的,但是在运行时,就会抛出空指针异常。其中,integer100为Integer类型的对象,它当然可以指向null。但在第二行时,就会对integer100进行拆箱,也就是对一个null对象执行intValue()方法,当然会抛出空指针异常。所以,有拆箱操作时一定要特别注意封装类对象是否为null。

陷阱2

[java] view plain copy
  1. Integer i1=100;  
  2. Integer i2=100;  
  3. Integer i3=300;  
  4. Integer i4=300;  
  5. System.out.println(i1==i2);  
  6. System.out.println(i3==i4);  

          因为i1、i2、i3、i4都是Integer类型的,所以我们想,运行结果应该都是false。但是,真实的运行结果为“System.out.println(i1==i2);”为 true,但是“System.out.println(i3==i4);”为false。也就意味着,i1与i2这两个Integer类型的引用指向了同一个对象,而i3与i4指向了不同的对象。为什么呢?不都是调用Integer.valueOf(int i)方法吗?

          让我们再看看Integer.valueOf(int i)方法。

[java] view plain copy
  1. /** 
  2.      * Returns a <tt>Integer</tt> instance representing the specified 
  3.      * <tt>int</tt> value. 
  4.      * If a new <tt>Integer</tt> instance is not required, this method 
  5.      * should generally be used in preference to the constructor 
  6.      * {@link #Integer(int)}, as this method is likely to yield 
  7.      * significantly better space and time performance by caching 
  8.      * frequently requested values. 
  9.      * 
  10.      * @param  i an <code>int</code> value. 
  11.      * @return a <tt>Integer</tt> instance representing <tt>i</tt>. 
  12.      * @since  1.5 
  13.      */  
  14.     public static Integer valueOf(int i) {  
  15.         if(i >= -128 && i <= IntegerCache.high)  
  16.             return IntegerCache.cache[i + 128];  
  17.         else  
  18.             return new Integer(i);  
  19.     }  

          我们可以看到当i>=-128且i<=IntegerCache.high时,直接返回IntegerCache.cache[i + 128]。其中,IntegerCache为Integer的内部静态类,其原码如下:

[java] view plain copy
  1. private static class IntegerCache {  
  2.         static final int high;  
  3.         static final Integer cache[];  
  4.   
  5.         static {  
  6.             final int low = -128;  
  7.   
  8.             // high value may be configured by property  
  9.             int h = 127;  
  10.             if (integerCacheHighPropValue != null) {  
  11.                 // Use Long.decode here to avoid invoking methods that  
  12.                 // require Integer's autoboxing cache to be initialized  
  13.                 int i = Long.decode(integerCacheHighPropValue).intValue();  
  14.                 i = Math.max(i, 127);  
  15.                 // Maximum array size is Integer.MAX_VALUE  
  16.                 h = Math.min(i, Integer.MAX_VALUE - -low);  
  17.             }  
  18.             high = h;  
  19.   
  20.             cache = new Integer[(high - low) + 1];  
  21.             int j = low;  
  22.             for(int k = 0; k < cache.length; k++)  
  23.                 cache[k] = new Integer(j++);  
  24.         }  
  25.   
  26.         private IntegerCache() {}  
  27.     }  

          我们可以清楚地看到,IntegerCache有静态成员变量cache,为一个拥有256个元素的数组。在IntegerCache中也对cache进行了初始化,即第i个元素是值为i-128的Integer对象。而-128至127是最常用的Integer对象,这样的做法也在很大程度上提高了性能。也正因为如此,“Integeri1=100;Integer i2=100;”,i1与i2得到是相同的对象。

          对比扩展中的第二个实验,我们得知,当封装类与基础类型进行==运行时,封装类会进行拆箱,拆箱结果与基础类型对比值;而两个封装类进行==运行时,与其它的对象进行==运行一样,对比两个对象的地址,也即判断是否两个引用是否指向同一个对象。



Java的自动拆箱和装箱是Java语言的一颗语法糖。在之前的学习中有很多误解,在别人的帮助下作出一些修正。先看下面的代码:

复制代码
 1 public static void main(String args[]) { 2         Integer a = 1; 3         Integer b = 2; 4         Integer c = 3; 5         Integer d = 3; 6         Integer e = 321; 7         Integer f = 321; 8         Long g = 3L; 9         int x = 3;10         long y = 3L;11         12         //x,y虽然类型不同但是可以直接进行数值比较13         System.out.println(x == y);14         //System.out.println(c == g); 提示出错,不可比较的类型。说明此时没有自动拆箱15         System.out.println(c == d);16         System.out.println(e == f);17         System.out.println(c == (a+b));18         System.out.println(c.equals(a+b));19         //此时进行了自动的拆箱20         System.out.println(g == (a+b));21         System.out.println(g.equals(a+b));22     }
复制代码

答案是:

T

T

F

T

T

T

F


这样的答案是不是出乎很多人的意料呢?我们一一来分析。

1. 首先我们明确一下"=="和equals方法的作用。

  "==":如果是基本数据类型,则直接对值进行比较,如果是引用数据类型,则是对他们的地址进行比较(但是只能比较相同类型的对象,或者比较父类对象和子类对象。类型不同的两个对象不能使用==)

  equals方法继承自Object类,在具体实现时可以覆盖父类中的实现。看一下Object中qeuals的源码发现,它的实现也是对对象的地址进行比较,此时它和"=="的作用相同。而JDK类中有一些类覆盖了Object类的equals()方法,比较规则为:如果两个对象的类型一致,并且内容一致,则返回true,这些类有:
java.io.file,java.util.Date,java.lang.string,包装类(Integer,Double等)。

2. Java的包装类实现细节。观察源码会发现Integer包装类中定义了一个私有的静态内部类如下:

复制代码
 1 private static class IntegerCache { 2     static final int low = -128; 3     static final int high; 4     static final Integer cache[]; 5  6     static { 7         // high value may be configured by property 8         int h = 127; 9         String integerCacheHighPropValue =10             sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");11         if (integerCacheHighPropValue != null) {12             try {13                 int i = parseInt(integerCacheHighPropValue);14                 i = Math.max(i, 127);15                 // Maximum array size is Integer.MAX_VALUE16                 h = Math.min(i, Integer.MAX_VALUE - (-low) -1);17             } catch( NumberFormatException nfe) {18                 // If the property cannot be parsed into an int, ignore it.19             }20         }21         high = h;22 23         cache = new Integer[(high - low) + 1];24         int j = low;25         for(int k = 0; k < cache.length; k++)26             cache[k] = new Integer(j++);27 28         // range [-128, 127] must be interned (JLS7 5.1.7)29         assert IntegerCache.high >= 127;30     }31 32     private IntegerCache() {}33 }
复制代码

而Integer的自动装箱代码:

1 public static Integer valueOf(int i) {2     if (i >= IntegerCache.low && i <= IntegerCache.high)3         return IntegerCache.cache[i + (-IntegerCache.low)];4     return new Integer(i);5 }

通过观察上面的代码我们可以发现,Integer使用一个内部静态类中的一个静态数组保存了-128-127范围内的数据,静态数组在类加载以后是存在方法区的,并不是什么常量池。在自动装箱的时候,首先判断要装箱的数字的范围,如果在-128-127的范围则直接返回缓存中已有的对象,否则new一个新的对象。其他的包装类也有类似的实现方式,可以通过源码观察一下。

3. "=="在遇到非算术运算符的情况下不会自动拆箱,以及他们的equals方法不处理数据类型转换的关系。

因此,对于 System.out.println(c == d); 他们指向同一个对象,返回True。

对于 System.out.println(e == f); 他们的值大于127,即使值相同,但是对应不同的内存地址,返回false。

对于 System.out.println(c == (a+b)); 自动拆箱后他们的值是相等的,返回True。

对于 System.out.println(c.equals(a+b)); 他们的值相同,而且类型相同,返回true。

对于 System.out.println(g == (a+b)); 自动拆箱后他们的值相等,返回True。

对于 System.out.println(g.equals(a+b)); 他们的值相同但是类型不同,返回false。

4. 总结

对于不懂的地方,最好是通过阅读源码的方式来解决。这样才能真正明白内部的一些实现方式。



原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 微信电话费充错了怎么办 支付宝电话费充错了怎么办 在淘宝上充错电话费了怎么办 话费1000充错了怎么办 东西掉在地铁上怎么办 高铁安检丢东西怎么办 东西掉成都地铁上怎么办 东西掉在成都地铁上怎么办 成都地铁上掉东西了怎么办 地铁站丢了东西怎么办 在地铁站丢了东西怎么办 没有签劳动合同不发工资怎么办 没有劳动合同辞职不给工资怎么办 地铁安检要交押金怎么办 在广州地铁上人走丢了怎么办 海尔全自动洗衣机程系乱了怎么办 河南危险化学品经营许可证怎么办 甲方不给付监理费怎么办 甲方不按合同付工程款怎么办 撞车对方全责不赔钱怎么办 电梯坏了没人修怎么办 电工超作证丢了怎么办 设计师直接找电梯厂家怎么办 研究生补助申请期限过了怎么办 我的电脑图标没了怎么办 苹果锁频密码忘了怎么办 孕妇被降职降薪怎么办 公司降职降薪员工不同意怎么办 企业因为经营不善要降薪该怎么办 调岗不降薪我该怎么办? 怀孕后强制调岗怎么办 有限公司法人变更后债务怎么办 有限公司法人跑路债务怎么办 网上买票身份信息待核验怎么办 微信买票身份核验失败怎么办 买高铁票待核验怎么办 网上购票身份待核验怎么办 b站稿件版权原因怎么办 已离职老板打电话说账有问题怎么办 开到应急刹车道怎么办 自动挡的车刹车失灵怎么办