深入理解Java中的装箱与拆箱
什么是装箱拆箱?
Java为每种基本的数据类型都提供了对应的包装器类型。在JavaSE5之前,如果要生成一个数值为10的Integer的对象,必须要这样进行:
Integer i = new Integer(10);
而在JavaSE5之后就提供了自动装箱的特性,可以写成:
Integer i = 10;
这个过程中会根据数值创建对应的Integer对象,这就是装箱。
那么什么是拆箱?就是反过来,自动将包装器类型转换为基本数据类型:
Integer i = 10;//装箱
int i = 10;//拆箱
下表是基本数据类型对应的包装器类型:
| int(4字节) | Integer |
|---|---|
| byte(1字节) | Byte |
| short(2字节) | Short |
| long(8字节) | Long |
| float(4字节) | Float |
| double(8字节) | Double |
| char(2字节) | Character |
| boolean(未定) | Boolean |
装箱是如何实现的?
以Integer为例:
public class Main {
public static void main(String[] args) {
Integer i = 10;
int n = i;
}
}
反编译class文件之后得到如下内容:

从字节码的内容可以看出,在装箱的时候自动调用Integer的valueOf(int)方法,而在拆箱的时候自动调用的是Integer的intValue方法,其他的比如Double、Character也类似。
因此可以用一句话总结装箱和拆箱的实现过程:
装箱过程是通过调用包装器的valueOf方法实现的,而拆箱过程是通过调用包装器的 xxxValue方法实现的。(xxx代表对应的基本数据类型)。
面试中的相关问题
下面的代码的输出结果是什么?
public class Main {
public static void main(String[] args) {
Integer i1 = 100;
Integer i2 = 100;
Integer i3 = 200;
Integer i4 = 200;
System.out.println(i1==i2);
System.out.println(i3==i4);
}
}
最后输出的结果是true;false;,结果表明i1和i2指向的是同一个对象,而i3和i4却不是。
下面的代码是Integer的ValueOf方法的具体实现:
public static Integer valueOf(int i) {
if(i >= -128 && i <= IntegerCache.high)
return IntegerCache.cache[i + 128];
else
return new Integer(i);
}
其中IntegerCache类的实现为:
private static class IntegerCache {
static final int high;
static final Integer cache[];
static {
final int low = -128;
// high value may be configured by property
int h = 127;
if (integerCacheHighPropValue != null) {
// Use Long.decode here to avoid invoking methods that
// require Integer's autoboxing cache to be initialized
int i = Long.decode(integerCacheHighPropValue).intValue();
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - -low);
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
}
private IntegerCache() {}
}
从这2段代码可以看出,在通过valueOf方法创建Integer对象的时候,如果数值在[-128,127]之间,便返回指向IntegerCache.cache中已经存在的对象的引用;否则创建一个新的Integer对象。
上面的代码中i1和i2的数值为100,因此会直接从cache中取已经存在的对象,所以i1和i2指向的是同一个对象,而i3和i4则是分别指向不同的对象。
下面这段代码的运行结果是什么?
public class Main {
public static void main(String[] args) {
Double i1 = 100.0;
Double i2 = 100.0;
Double i3 = 200.0;
Double i4 = 200.0;
System.out.println(i1==i2);
System.out.println(i3==i4);
}
}
与上一段代码不同的是,变量类型由Integer变成了Double,而运行结果也发生了变化,结果为false;false;
那么为什么运行结果会不同呢?因为Byte、Short、Integer、Long、Boolean、Character这六种包装类型在进行自动装箱时都使用了缓存策略,而Double与Float两种包装类型是浮点数,不像整数那样在某个范围内是有限的,所以它们没有使用缓存机制。下面是Double包装类的自动装箱的源代码:
public static Double valueOf(double d) {
return new Double(d);
}
自动装箱的弊端
Integer sum = 0;
for(int i=1000; i<10000; i++){
sum+=i;
}
如上代码,当在循环中对包装类型进行算数运算sum=sum+i;时,会先触发自动拆箱,进行加法运算后,再进行自动装箱,因为sum数值不在缓存的范围之内,所以每次都会new一个Integer实例,在循环结束后会在内存中产生9000个无用的Integer实例对象,这样会大大降低程序的性能,增加GC的开销。
所以我们在写循环语句时一定要正确地声明变量的类型,避免因为自动装箱而引起的不必要的性能问题。

浙公网安备 33010602011771号