Java的自动装箱/拆箱

概述

自JDK1.5开始, 引入了自动装箱/拆箱这一语法糖, 它使程序员的代码变得更加简洁, 不再需要进行显式转换。基本类型与包装类型在某些操作符的作用下, 包装类型调用valueOf()方法将原始类型值转换成对应的包装类对象的过程, 称之为自动装箱; 反之调用xxxValue()方法将包装类对象转换成原始类型值的过程, 则称之为自动拆箱。

实现原理

首先我们用javap -c AutoBoxingDemo命令将下面代码反编译: 

public class AutoBoxingDemo {
    public static void main(String[] args) {
        Integer m = 1;
        int n = m;
    }
}

反编译后结果:

Java的自动装箱/拆箱_第1张图片

从反编译后的字节码指令中可以看出, Integer m = 1; 其实底层就是调用了包装类Integer的valueOf()方法进行自动装箱, 而 int n = m; 则是底层调用了包装类的intValue()方法进行自动拆箱。

其中Byte、Short、Integer、Long、Boolean、Character这六种包装类型在进行自动装箱时都使用了缓存策略, 下面是Integer类的缓存实现机制: 

/**
 * This method will always cache values in the range -128 to 127,
 * inclusive, and may cache other values outside of this range.
 */
public static Integer valueOf(int i) {
    assert IntegerCache.high >= 127;
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

private static class IntegerCache {
    static final int low = -128;
    static final int high;
    static final Integer cache[];

    static {
        // high value may be configured by property
        int h = 127;
        String integerCacheHighPropValue =
            sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
        if (integerCacheHighPropValue != null) {
            int i = parseInt(integerCacheHighPropValue);
            i = Math.max(i, 127);
            // Maximum array size is Integer.MAX_VALUE
            h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
        }
        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() {}
}

从Integer的源代码我们能得知, 当进行自动装箱的数值在[-128, 127]之间时, 调用valueOf()方法返回的是Integer缓存中已存在的对象引用。否则每次都是new一个新的包装类实例。

而Double、Float这两种包装类型因为是浮点数, 不像整数那样在某个范围内的数值个数是有限的, 所以它们没有使用缓存实现机制, 下面是Double包装类的自动装箱的源代码: 

public static Double valueOf(double d) {
    return new Double(d);
}

举例说明

public class AutoBoxingDemo {
    public static void main(String[] args) {
        Integer a = 1;
        Integer b = 2;
        Integer c = 3;
        Integer d = 3;
        Integer e = 321;
        Integer f = 321;
        Long g = 3L;
        Long h = 2L;
        Double i = 1.0;  
        Double j = 1.0;
        Boolean k = true;
        Boolean l = true;
//数值在[-128, 127]范围内,自动装箱时都是从缓存中获取对象引用,所以结果为true System.out.println(c==d); //数值在[-128, 127]范围外,自动装箱时每次都是new新的对象,所以结果为false System.out.println(e==f); //当"=="运算符的两个操作数都是包装器类型的引用,则比较指向的是否是同一个对象,而如果其中有一个操作数是表达式(即包含算术运算)则比较的是数值(即会触发自动拆箱的过程), 所以结果为true System.out.println(c==(a+b)); //对于包装类型,当equals()方法比较的是同一类型时(比如Integer与Integer比较),实际比较的是他们的数值是否相等。如比较的不是同一类型,则不会进行类型转换,直接返回false。所以结果为true System.out.println(c.equals(a+b)); //因为有算术运算,自动拆箱后再比较数值,所以结果为true System.out.println(g==(a+b)); //因为equals()方法比较的是不同包装类型,不会进行类型转换,所以结果为false System.out.println(g.equals(a+b)); //因为a+h先触发自动拆箱,a转为int类型后,需要隐式向上提升类型为long后再进行运算,最后再自动装箱转为Long包装类型,且两边数值相等,所以结果为true System.out.println(g.equals(a+h)); //Double类没有缓存,每次都是new一个新的实例,所以结果为false System.out.println(i == j); //Boolean自动装箱,指向的都是同一个实例,所以结果为true System.out.println(k == l); } }

在上面示例中, 关于结果的解析已经阐述的很清楚了, 主要有两个地方具有迷惑性。当"=="运算符的两个操作数都是包装器类型的引用,则比较指向的是否是同一个对象,而如果其中有一个操作数是表达式(即包含算术运算)则比较的是数值(即会先触发自动拆箱的过程)。

对于包装类型,当equals()方法比较的是同一类型时(比如Integer与Integer比较), 实际比较的是他们的数值是否相等; 如比较的不是同一类型(比如Integer与Long比较), 则不会进行类型转换,直接返回false。下面是Integer类的equals()方法的源代码: 

public boolean equals(Object obj) {
    if (obj instanceof Integer) {
        return value == ((Integer)obj).intValue();
    }
    return false;
}

另外我们也可以反编译以上代码, 穿透语法糖的糖衣能帮助我们更容易了解这些具有迷惑性现象的背后原理: 

Java的自动装箱/拆箱_第2张图片

自动装箱/拆箱带来的问题

自动拆箱下算术运算引起的空指针问题

private Double distinct;
private void setParam(Double dSrc, boolean flag) {
    this.distinct = (flag) ? dSrc : 0d;
}

上面这段代码乍一看是没问题的, 但实际当dSrc为null时, 调用该方法会抛出空指针异常, 我们对其进行反编译:

Java的自动装箱/拆箱_第3张图片可以看出, 当对包装类进行诸如三目运算符的算术运算时, 当数据类型不一致时, 编译器会自动拆箱转换为基本类型再进行运算, 所以当dSrc传入null值时, 调用doubleValue()方法拆箱就会报NP空指针异常。

这里我们可以在进行算术运算时, 统一数据类型, 避免编译器进行自动拆箱, 来解决拆箱下三目运算符的空指针问题。还是上面这个栗子, 我们将 this.distinct = (flag) ? dSrc : 0d; 修改成 this.distinct = (flag) ? dSrc : Double.valueOf(0); 即可解决, 重新反编译后如下, 因为类型一致, 没有再进行自动拆箱: 

自动装箱的弊端 

Integer sum = 0;
 for(int i=1000; i<10000; i++){
   sum+=i;
}

如上代码, 当在循环中对包装类型进行算术运算 sum = sum + i; 时, 会先触发自动拆箱, 进行加法运算后, 再进行自动装箱,  且因为运算后的sum数值不在缓存范围之内, 所以每次都会new一个新的Integer实例。所以上面的循环结束后, 将会在内存中创建9000个无用的Integer实例对象, 这样会大大降低程序的性能, 增加GC的开销, 所以我们在写循环语句时一定要正确的声明变量类型, 避免因为自动装箱而引起不必要的性能问题。

重载与自动装箱

在JDK1.5之前, 没有引入自动装箱/拆箱这一语法糖, 当方法重载时,  test(int num) 与 test(Integer num) 的形参没有任何关系。JDK1.5之后, 当调用重载的方法时, 编译器不会进行自动装箱操作, 我们可以通过运行下面的代码示例来演示。

public static void testAutoBoxing(int num) {
    System.out.println("方法形参为原始类型");
}

public static void testAutoBoxing(Integer num) {
    System.out.println("方法形参为包装类型");
}

public static void main(String[] args) {
    int m = 2;
    testAutoBoxing(m);
    Integer n = m;
    testAutoBoxing(n);
}

运行结果如下: 

Java的自动装箱/拆箱_第4张图片

很明显, 当调用重载的方法时, 编译器不会对传入的实参进行自动装箱操作。

参考资料

Autoboxing and Unboxing (The Java Tutorials > Lea...

深入剖析Java中的装箱和拆箱 - 海 子 - 博客园

Java 自动装箱与拆箱的实现原理 - 简书

Java自动拆箱下, 三目运算符的潜规则

Java中的自动装箱与拆箱

你可能感兴趣的:(Java的自动装箱/拆箱)