自动装箱/拆箱中的陷阱

2018-08-06  本文已影响0人  shysheng

最近看到一个问题,很有意思,算是击中了自己的一个知识盲区,因此发出来跟大家分享一下。
我们知道,在java中,== 运算符比较的是两个对象的引用,同时对一些经常需要用打的小对象(-128~127),在内存中是复用的,因此在下面这段代码中,将分别输出true和false。

public static void main(String[] args) {
    Integer a = 1;
    Integer b = 1;
    Integer c = 1000;
    Integer d = 1000;

    System.out.println(a == b); // true
    System.out.println(c == d); // false
}

基于这个认识,当我第一次看到下面这份代码时,我本能地认为将输出false和true,然而实际运行结果却让我大跌眼镜,第一个表达式的结果竟然也是true!!!难道说一个Long型常量3和一个Integer型常量3指向的是同一个引用?

public class BoxTest {

    public static void main(String[] args) {
        Integer a = 1;
        Integer b = 2;
        Long c = 3L;
        Long d = 3L;

        System.out.println(c == a + b); // true
        System.out.println(c == d); // true
    }
}

为了一探究竟,我决定先反编译下对应的class文件,结果如下。可以看到,表达式c == a + b经过编译后,Long型常量和Integer型常量都被自动拆箱了,这时==运算符比较的其实是两者的数值大小。这跟自己之前了解到的似乎不太一样,于是决定继续看看对应的字节码作进一步的验证。

public class BoxTest {
    public BoxTest() {
    }

    public static void main(String[] args) {
        Integer a = Integer.valueOf(1);
        Integer b = Integer.valueOf(2);
        Long c = Long.valueOf(3L);
        Long d = Long.valueOf(3L);
        System.out.println(c.longValue() == (long)(a.intValue() + b.intValue()));
        System.out.println(c == d);
    }
}

运用javap工具,得到对应的字节码如下。不难看出:
1.0~37字节是将4个常量做装箱操作并赋值给a/b/c/d;
2.40~41字节是将栈顶的a/b相加并强转为long型;
3.最关键的两个比较操作是42字节处的指令lcmp和60字节处的指令if_acmpne

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=5, locals=5, args_size=1
         0: iconst_1
         1: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
         4: astore_1
         5: iconst_2
         6: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
         9: astore_2
        10: ldc2_w        #3                  // long 3l
        13: invokestatic  #5                  // Method java/lang/Long.valueOf:(J)Ljava/lang/Long;
        16: astore_3
        17: ldc2_w        #3                  // long 3l
        20: invokestatic  #5                  // Method java/lang/Long.valueOf:(J)Ljava/lang/Long;
        23: astore        4
        25: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
        28: aload_3
        29: invokevirtual #7                  // Method java/lang/Long.longValue:()J
        32: aload_1
        33: invokevirtual #8                  // Method java/lang/Integer.intValue:()I
        36: aload_2
        37: invokevirtual #8                  // Method java/lang/Integer.intValue:()I
        40: iadd
        41: i2l
        42: lcmp
        43: ifne          50
        46: iconst_1
        47: goto          51
        50: iconst_0
        51: invokevirtual #9                  // Method java/io/PrintStream.println:(Z)V
        54: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
        57: aload_3
        58: aload         4
        60: if_acmpne     67
        63: iconst_1
        64: goto          68
        67: iconst_0
        68: invokevirtual #9                  // Method java/io/PrintStream.println:(Z)V
        71: return

lcmp和if_acmpne这两个操作指令的含义如下,跟我们猜想的一致。

lcmp    比较栈顶两long型数值大小, 并将结果(1, 0或-1)压入栈顶
if_acmpne   比较栈顶两引用型数值, 当结果不相等时跳转

从这个简单的例子中,我们发现,==运算符比较的并不一定总是对象的引用,当遇到算数运算符时,经过装箱的对象将会被拆箱,这时实际比较的将是两者的数值大小。

上一篇下一篇

猜你喜欢

热点阅读