Java自动装箱与拆箱及其陷阱

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

装箱(Autoboxing)

          你们在平时编写Java程序时,都经常以如下方式来定义一个Integer对象: 数组

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

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

          在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)方法中 ui

[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)。 this

拆箱(Unboxing)

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

          从上面的代码中,你们可看出integer100为一个Integer类型的引用,int100为一个int类型的原始数据类型。可是,咱们能够将一个Integer类型的对象赋值给其相应原始数据类型的变量。这即是拆箱。 spa

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

原理

          笔者相信你们也都猜到了,拆箱过程当中jdk为咱们作了什么。咱们仍是经过实验来证实咱们的猜测吧。 指针

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

          咱们能够看到,程序跳转到了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);  

          由于i一、i二、i三、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获得是相同的对象。

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

相关文章
相关标签/搜索