自动装箱的陷阱

84 阅读4分钟

问题引出

案例

在《深入理解java虚拟机》中,有这样一个案例

运行结果如注释

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;
    
    System.out.println(c==d);                  //case1:true
    System.out.println(e==f);                  //case2:false
    System.out.println(c==(a+b));              //case3:true
    System.out.println(c.equals(a+b));         //case4:true
    System.out.println(g==(a+b));              //case5:true
    System.out.println(g.equals(a+b));         //case6:false
}

在书中并没有给出运行结果和具体分析,给出了两点提示:

  • == 在不遇到算术运算时不会自动装箱
  • 他们的equal()方法不处理数据转型的关系

疑惑

初看运行结果,有着较多疑惑:

  • 疑惑1: ==在Java中对于对象而言是比较对象的地址,为何case1中结果为true
  • 疑惑2: case1与case2同为Integar类型的比较,为何case1结果为true,case2结果为false
  • 疑惑3: c,g的值都为3,为何在使用equal()方法与a+b比较二者运行结果case4为true,case6为false

问题解决

疑惑1与疑惑2

实质上,疑惑1与疑惑2是相同的原因导致的,因此放在一起解答。

这里用一个最简单的小案例进行解释

public void testAutoBoxing() {
    Integer i=1;
}

使用编译器的小伙伴们不妨在Integer类的 valueOf(int i)上打上断点。

在调试模式下运行上方的代码,会发现跳到Integer类的 valueOf(int i)上(为何会跳到此暂且不谈)

image.png

可以看出在创建Integar对象自动装箱时,并不是直接new一个新的Integer对象,而是先判断是否在cache范围内。若是:直接返回cache中的对象;若不是:创建新的Integer对象。

让我们来看一下上图中IntegerCache类的源码,就在valueOf(int 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) {
            try {
                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);
            } catch( NumberFormatException nfe) {
                // If the property cannot be parsed into an int, ignore it.
            }
        }
        high = h;

        cache = new Integer[(high - low) + 1];
        int j = low;
        for(int k = 0; k < cache.length; k++)
            cache[k] = new Integer(j++);

        // range [-128, 127] must be interned (JLS7 5.1.7)
        assert IntegerCache.high >= 127;
    }

    private IntegerCache() {}
}

可以看出,静态类IntegerCache中储存了值为-128~high 个Integer对象,其中high可以由用户自行配置,默认为127。同理LongCache以及其他包装类也是如此。

这样一来,疑惑1和疑惑2就迎刃而解了:

  • 疑惑1:由于c与d的值为3,都在-128到127的范围内,因此实质上都指向了IntegerCache中存储的同一个对象,地址自然相同
  • 疑惑2:由于e和f的值为321,超出了IntegerCache的界限,因此为e和f分别创建了不同的Integer对象,地址自然不同

还记的刚才分析过程中遗留的问题吗?为何会i的赋值跳到Integer类的valueOf(int i)方法上

不妨深入testAutoBoxing()看看:

public void testAutoBoxing();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=2, args_size=1
         0: iconst_1
         1: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
         4: astore_1
         5: return
      LineNumberTable:
        line 36: 0
        line 37: 5
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       6     0  this   Lchapter_10/TestEqual;
            5       1     1     i   Ljava/lang/Integer;
    RuntimeVisibleAnnotations:
      0: #46()

在上面代码的第7行,为i赋值时,编译器为我们执行并不是Integer()构造器方法,而是valueOf()方法。 之所以这样做的原因,可以给出以下解释:

在-128~high范围内的Integer类型变量较常使用,因此用cache缓存这些变量,编译器为我们作了优化,装箱时调用valueOf()方法,在创建前先进行判断是否是在该范围内,是则将变量直接指向cache中缓存的Integer,这种优化在大型项目中起到了节省空间的作用。

疑惑3

  • 疑惑3: c,g的值都为3,为何在使用equal()方法与a+b比较二者运行结果case4为true,case6为false

同样的,写一个简单的案例:

public void testLongAndIntegerEqual() {
    Integer a=1;
    Long b=1L;
    System.out.println(b.equals(a));
}

看看Long类的equal()源码:

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

可以看出,Long类中没有提供对其他包装类的转换,如果与其比较的不是同为Long类型的对象,直接返回false

总结

最后,我们再通过注释的方式解释下最初的案例

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;
    
    System.out.println(c==d);                  //case1:指向同一个cache对象,因此返回true
    System.out.println(e==f);                  //case2:指向不同对象,因此返回false
    System.out.println(c==(a+b));              //case3:在(a+b)运算中发生了自动拆箱与装箱,
                                               //       (a+b)与c指向对象相等,因此返回true
    System.out.println(c.equals(a+b));         //case4:在(a+b)运算中发生了自动装箱,
                                               //       装箱后的(a+b)与c指向相同对象,因此返回true
    System.out.println(g==(a+b));              //case5:在(a+b)运算中发生了自动拆箱与装箱,
                                               //       (a+b)与g指向对象相等,因此返回true
    System.out.println(g.equals(a+b));         //case6:在(a+b)运算中发生了自动装箱,转化为Integer类型
                                               //       装箱后的(a+b)与c为不同类型对象,直接返回false
}