深入理解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文件之后得到如下内容:

006ozJEaly1ggrpggl023j30m50a80uc.jpg

从字节码的内容可以看出,在装箱的时候自动调用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的开销。

所以我们在写循环语句时一定要正确地声明变量的类型,避免因为自动装箱而引起的不必要的性能问题。

posted @ 2021-03-13 16:49  皆守  阅读(190)  评论(0)    收藏  举报