JAVA自动拆装箱

Java有8种基本类型,每种基本类型又有对应的包装类型。在Java中,一切都以对象作为基础,但是基本类型并不是对象,如果想以对象的方式使用这8中基本类型,可以将它们转换为对应的包装类型。基本类型和包装类型的对应:

int(4字节) Integer
byte(1字节) Byte
short(2字节) Short
long(8字节) Long
float(4字节) Float
double(8字节) Double
char(2字节) Character
boolean(未定) Boolean

Java 5增加了自动装箱与自动拆箱机制,方便基本类型与包装类型的相互转换操作。在Java 5之前,如果要将一个int型的值转换成对应的包装器类型Integer,必须显式的使用new创建一个新的Integer对象,或者调用静态方法Integer.valueOf()。

[java]  view plain  copy
  1. //在Java 5之前,只能这样做  
  2. Integer value = new Integer(10);  
  3. //或者这样做  
  4. Integer value = Integer.valueOf(10);  
  5. //直接赋值是错误的  
  6. //Integer value = 10;  

在Java 5中,可以直接将整型赋给Integer对象,由编译器来完成从int型到Integer类型的转换,这就叫自动装箱。

[java]  view plain  copy
  1. //在Java 5中,直接赋值是合法的,由编译器来完成转换  
  2. Integer value = 10;  
与此对应的,自动拆箱就是可以将包装类型转换为基本类型,具体的转换工作由编译器来完成。
[java]  view plain  copy
  1. //在Java 5 中可以直接这么做  
  2. Integer value = new Integer(10);  
  3. int i = value;  
自动装箱与自动拆箱为程序员提供了很大的方便,而在实际的应用中,自动装箱与拆箱也是使用最广泛的特性之一。自动装箱和自动拆箱其实是Java编译器提供的一颗语法糖(语法糖是指在计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。通过可提高开发效率,增加代码可读性,增加代码的安全性)。

1 实现

在八种包装类型中,每一种包装类型都提供了两个方法:

静态方法valueOf(基本类型):将给定的基本类型转换成对应的包装类型;

实例方法xxxValue():将具体的包装类型对象转换成基本类型;
下面我们以int和Integer为例,说明Java中自动装箱与自动拆箱的实现机制。看如下代码:

[java]  view plain  copy
  1. class Auto //code1  
  2. {  
  3.     public static void main(String[] args)   
  4.     {  
  5.         //自动装箱  
  6.         Integer inte = 10;  
  7.         //自动拆箱  
  8.         int i = inte;  
  9.   
  10.         //再double和Double来验证一下  
  11.         Double doub = 12.40;  
  12.         double d = doub;  
  13.           
  14.     }  
  15. }  
上面的代码先将int型转为Integer对象,再讲Integer对象转换为int型,毫无疑问,这是可以正确运行的。可是,这种转换是怎么进行的呢?使用反编译工具,将生成的Class文件在反编译为Java文件,让我们看看发生了什么:
[java]  view plain  copy
  1. class Auto//code2  
  2. {  
  3.   public static void main(String[] paramArrayOfString)  
  4.   {  
  5.     Integer localInteger = Integer.valueOf(10);  
  6.       
  7.     int i = localInteger.intValue();  
  8.       
  9.   
  10.     Double localDouble = Double.valueOf(12.4D);  
  11.     double d = localDouble.doubleValue();  
  12.   }  
  13. }  
我们可以看到经过javac编译之后,code1的代码被转换成了code2,实际运行时,虚拟机运行的就是code2的代码。也就是说,虚拟机根本不知道有自动拆箱和自动装箱这回事;在将Java源文件编译为class文件的过程中,javac编译器在自动装箱的时候,调用了Integer.valueOf()方法,在自动拆箱时,又调用了intValue()方法。我们可以看到,double和Double也是如此。

实现总结:其实自动装箱和自动封箱是编译器为我们提供的一颗语法糖。在自动装箱时,编译器调用包装类型的valueOf()方法;在自动拆箱时,编译器调用了相应的xxxValue()方法。

2 自动装箱与拆箱中的“坑”

在使用自动装箱与自动拆箱时,要注意一些陷阱,为了避免这些陷阱,我们有必要去看一下各种包装类型的源码。

Integer源码

[java]  view plain  copy
  1. public final class Integer extends Number implements Comparable<Integer> {  
  2.     private final int value;  
  3.       
  4.     /*Integer的构造方法,接受一个整型参数,Integer对象表示的int值,保存在value中*/  
  5.      public Integer(int value) {  
  6.             this.value = value;  
  7.      }  
  8.        
  9.     /*equals()方法判断的是:所代表的int型的值是否相等*/  
  10.      public boolean equals(Object obj) {  
  11.             if (obj instanceof Integer) {  
  12.                 return value == ((Integer)obj).intValue();  
  13.             }  
  14.             return false;  
  15.     }  
  16.        
  17.     /*返回这个Integer对象代表的int值,也就是保存在value中的值*/  
  18.      public int intValue() {  
  19.             return value;  
  20.      }  
  21.        
  22.      /** 
  23.       * 首先会判断i是否在[IntegerCache.low,Integer.high]之间 
  24.       * 如果是,直接返回Integer.cache中相应的元素 
  25.       * 否则,调用构造方法,创建一个新的Integer对象 
  26.       */  
  27.      public static Integer valueOf(int i) {  
  28.         assert IntegerCache.high >= 127;  
  29.         if (i >= IntegerCache.low && i <= IntegerCache.high)  
  30.             return IntegerCache.cache[i + (-IntegerCache.low)];  
  31.         return new Integer(i);  
  32.      }  
  33.       
  34.     /** 
  35.       * 静态内部类,缓存了从[low,high]对应的Integer对象 
  36.       * low -128这个值不会被改变 
  37.       * high 默认是127,可以改变,最大不超过:Integer.MAX_VALUE - (-low) -1 
  38.       * cache 保存从[low,high]对象的Integer对象 
  39.      */  
  40.      private static class IntegerCache {  
  41.         static final int low = -128;  
  42.         static final int high;  
  43.         static final Integer cache[];  
  44.   
  45.         static {  
  46.             // high value may be configured by property  
  47.             int h = 127;  
  48.             String integerCacheHighPropValue =  
  49.                 sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");  
  50.             if (integerCacheHighPropValue != null) {  
  51.                 int i = parseInt(integerCacheHighPropValue);  
  52.                 i = Math.max(i, 127);  
  53.                 // Maximum array size is Integer.MAX_VALUE  
  54.                 h = Math.min(i, Integer.MAX_VALUE - (-low) -1);  
  55.             }  
  56.             high = h;  
  57.   
  58.             cache = new Integer[(high - low) + 1];  
  59.             int j = low;  
  60.             for(int k = 0; k < cache.length; k++)  
  61.                 cache[k] = new Integer(j++);  
  62.         }  
  63.   
  64.         private IntegerCache() {}  
  65.     }  
  66. }  

以上是Oracle(Sun)公司JDK 1.7中Integer源码的一部分,通过分析上面的代码,得到:
1)Integer有一个实例域value,它保存了这个Integer所代表的int型的值,且它是final的,也就是说这个Integer对象一经构造完成,它所代表的值就不能再被改变。
2)Integer重写了equals()方法,它通过比较两个Integer对象的value,来判断是否相等。
3)重点是静态内部类IntegerCache,通过类名就可以发现:它是用来缓存数据的。它有一个数组,里面保存的是连续的Integer对象。
   (a) low:代表缓存数据中最小的值,固定是-128。
   (b) high:代表缓存数据中最大的值,它可以被该改变,默认是127。high最小是127,最大是Integer.MAX_VALUE-(-low)-1,如果high超过了这个值,那么cache[ ]的长度就超过Integer.MAX_VALUE了,也就溢出了。
   (c) cache[]:里面保存着从[low,high]所对应的Integer对象,长度是high-low+1(因为有元素0,所以要加1)。
4)调用valueOf(int i)方法时,首先判断i是否在[low,high]之间,如果是,则复用Integer.cache[i-low]。比如,如果Integer.valueOf(3),直接返回Integer.cache[131];如果i不在这个范围,则调用构造方法,构造出一个新的Integer对象。
5)调用intValue(),直接返回value的值。
通过3)和4)可以发现,默认情况下,在使用自动装箱时,VM会复用[-128,127]之间的Integer对象。

[java]  view plain  copy
  1. Integer  a1 = 1;  
  2. Integer  a2 = 1;  
  3. Integer  a3 = new Integer(1);  
  4. //会打印true,因为a1和a2是同一个对象,都是Integer.cache[129]  
  5. System.out.println(a1 == a2);  
  6. //false,a3构造了一个新的对象,不同于a1,a2  
  7. System.out.println(a1 == a3);  

Byte源码

[java]  view plain  copy
  1. public final class Byte extends Number implements Comparable<Byte> {  
  2.         //Byte表示的范围是[-128,127]  
  3.         public static final byte   MIN_VALUE = -128;  
  4.         public static final byte   MAX_VALUE = 127;  
  5.           
  6.         private final byte value;  
  7.           
  8.         public Byte(byte value) {  
  9.             this.value = value;  
  10.         }  
  11.           
  12.         /** 
  13.          * 缓存Byte对象 
  14.          * 将Byte可能的256个对象全部保存到cache[]中 
  15.          * @author cxy 
  16.          * 
  17.          */  
  18.         private static class ByteCache {  
  19.             private ByteCache(){}  
  20.   
  21.             static final Byte cache[] = new Byte[-(-128) + 127 + 1];  
  22.   
  23.             static {  
  24.                 for(int i = 0; i < cache.length; i++)  
  25.                     cache[i] = new Byte((byte)(i - 128));  
  26.             }  
  27.         }  
  28.           
  29.         /*直接返回ByteCache.cache[]中相应的对象*/  
  30.         public static Byte valueOf(byte b) {  
  31.             final int offset = 128;  
  32.             return ByteCache.cache[(int)b + offset];  
  33.         }  
  34.           
  35.         /*返回此对象的byte值*/  
  36.         public byte byteValue() {  
  37.             return value;  
  38.         }  
  39.     }  
byte的表示范围是[-128,127],在Byte内部同样有一个ByteCache类,它也同样有一个cache[ ],它里面保存了所有可能的256个Byte对象。所以在自动装箱时,所有的Byte对象都是复用ByteCache.cache[ ]中的元素。

同样的Character中的CharacterCache类也有一个cache[ ],缓存了[0,127]中的元素。Short和Integer一样,缓存了[-128,127]之间的数,不同的是,Integer可以修改high的值,ShortCache中则是写死的,不能改变。Long的实现方法和Short一样。

Double和Float

[java]  view plain  copy
  1. /*Double.valueOf(double d)*/  
  2.   public static Double valueOf(Double d) {  
  3.         return new Double(d);  
  4.     }  
  5.   
  6. /*Float.valueOf(float f)*/  
  7.   public static Float valueOf(float f) {  
  8.         return new Float(f);  
  9.     }  
从源码中可以看出,Double和Float都没有缓存了,调用valueOf()方法,直接构造出一个新的对象。Double和Float之所以不用缓存,是因为没有办法缓存,(0,1)这么小的一个区间里面,就有无数个double或float数,根本无从缓存。所以在使用Double和Float自动装箱时,全都是构造新的对象,没有缓存。

Boolean源码

[java]  view plain  copy
  1. public final class Boolean implements java.io.Serializable,Comparable<Boolean>  
  2. {  
  3.     /*boolean只有两种取值:true,false,所以不需要内部类来缓存了*/  
  4.     public static final Boolean TRUE = new Boolean(true);  
  5.     public static final Boolean FALSE = new Boolean(false);  
  6.   
  7.     private final boolean value;  
  8.   
  9.     public Boolean(boolean value) {  
  10.         this.value = value;  
  11.     }  
  12.   
  13.     public boolean booleanValue() {  
  14.         return value;  
  15.     }  
  16.       
  17.     /** 
  18.      * 根据b的值,返回对应的对象 
  19.      */  
  20.     public static Boolean valueOf(boolean b) {  
  21.         return (b ? TRUE : FALSE);  
  22.     }  
  23. }  
查看Boolean的源码,发现Boolean没有无参的valueOf(),我们可以推断Boolean没有自动装箱与封箱,可以通过代码验证一下:
[java]  view plain  copy
  1. boolean b = true;  
  2. Boolean b1 = b;  
  3. boolean b2 = b1;  
这些代码是无法通过编译的。

Boolean还是用到了缓存,由于boolean只有两种取值,所以没有必要使用内部类或者数组来保存缓存的对象,直接定义两个静态属性即可,也就是Boolean.TRUE和Boolean.FALSE。在调用Boolean.valueOf(boolean b)是,返回的是缓存的TRUE或者FALSE,代码验证:

[java]  view plain  copy
  1. Boolean b1 = Boolean.valueOf(true);  
  2. Boolean b2 = Boolean.valueOf(true);  
  3. Boolean b3 = new Boolean(true);  
  4. //true,因为返回的都是TRUE对象  
  5. System.out.println(b1 == b2);  
  6. //false,因为b1是TRUE,b3则是一个新的Boolean对象  
  7. System.out.println(b1 == b3);  

发生时机

来欣赏一个比较典型的例子:

[java]  view plain  copy
  1. public class AutoWrapperTrap {  
  2.     public static void main(String[] args) {  
  3.         //[-128,127]之间,自动装箱会复用对象  
  4.         Integer a = 1;  
  5.         Integer b = 2;  
  6.         Integer c = 3;  
  7.         Integer d = 3;  
  8.         //不会复用  
  9.         Integer e = 321;  
  10.         Integer f = 321;  
  11.           
  12.         int base = 3;  
  13.           
  14.         Long g = 3L;  
  15.           
  16.         System.out.println(c == base);//true c自动拆箱  
  17.         System.out.println(c == d);//true  
  18.         System.out.println(e == f);//false  
  19.         System.out.println(c == (a + b));//true 遇到算术运算,自动拆箱  
  20.         System.out.println(c.equals(a + b));//true 需要对象,自动装箱  
  21.         System.out.println(g == (a + b));//true  
  22.         System.out.println(g.equals(a + b));//false 只会自动装箱为对应的包装类型  
  23.     }  
  24. }  

通过反编译后,得到如下代码:
[java]  view plain  copy
  1. public class AutoWrapperTrap  
  2. {  
  3.   public static void main(String[] args)  
  4.   {  
  5.     Integer a = Integer.valueOf(1);  
  6.     Integer b = Integer.valueOf(2);  
  7.     Integer c = Integer.valueOf(3);  
  8.     Integer d = Integer.valueOf(3);  
  9.   
  10.   
  11.     Integer e = Integer.valueOf(321);  
  12.     Integer f = Integer.valueOf(321);  
  13.   
  14.   
  15.     int base = 3;  
  16.   
  17.   
  18.     Long g = Long.valueOf(3L);  
  19.   
  20.   
  21.     System.out.println(c.intValue() == base);  
  22.     System.out.println(c == d);  
  23.     System.out.println(e == f);  
  24.     System.out.println(c.intValue() == a.intValue() + b.intValue());  
  25.     System.out.println(c.equals(Integer.valueOf(a.intValue() + b.intValue())));  
  26.     System.out.println(g.longValue() == a.intValue() + b.intValue());  
  27.     System.out.println(g.equals(Integer.valueOf(a.intValue() + b.intValue())));  
  28.   }  
  29. }  
通过上面的代码,我们分析一下自动装箱与拆箱发生的时机:

(1)当需要一个对象的时候会自动装箱,比如Integer a = 10;equals(Object o)方法的参数是Object对象,所以需要装箱。

(2)当需要一个基本类型时会自动拆箱,比如int a = new Integer(10);算术运算是在基本类型间进行的,所以当遇到算术运算时会自动拆箱,比如代码中的 c == (a + b);

(3) 包装类型 == 基本类型时,包装类型自动拆箱;

需要注意的是:“==”在没遇到算术运算时,不会自动拆箱;基本类型只会自动装箱为对应的包装类型,代码中最后一条说明的内容。

总结

在JDK 1.5中提供了自动装箱与自动拆箱,这其实是Java 编译器的语法糖,编译器通过调用包装类型的valueOf()方法实现自动装箱,调用xxxValue()方法自动拆箱。自动装箱和拆箱会有一些陷阱,那就是包装类型复用了某些对象。

(1)Integer默认复用了[-128,127]这些对象,其中高位置可以修改;

(2)Byte复用了全部256个对象[-128,127];

(3)Short服用了[-128,127]这些对象;

(4)Long服用了[-128,127];

(5)Character复用了[0,127],Charater不能表示负数;

Double和Float是连续不可数的,所以没法复用对象,也就不存在自动装箱复用陷阱。

Boolean没有自动装箱与拆箱,它也复用了Boolean.TRUE和Boolean.FALSE,通过Boolean.valueOf(boolean b)返回的Blooean对象要么是TRUE,要么是FALSE,这点也要注意。

本文介绍了“真实的”自动装箱与拆箱,为了避免写出错误的代码,又从包装类型的源码入手,指出了各种包装类型在自动装箱和拆箱时存在的陷阱,同时指出了自动装箱与拆箱发生的时机。

转载请注明出处:喻红叶《Java的装箱与拆箱机制》

猜你喜欢

转载自blog.csdn.net/lldouble/article/details/80748890