详解Java中自动装箱拆箱

一段代码引发的问题

最近在学习一本关于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);//true
        System.out.println(e == f);//false
        System.out.println(c == (a + b));//true
        System.out.println(c.equals(a + b));//true
        System.out.println(g == (a + b));//true
        System.out.println(g.equals(a + b));//false
    }

作者并没有给出输出答案是如何,自己分析了一下,自认为应该没错,然后实践运行出来傻眼了,暗恨自己以前囫囵吞枣,没有彻底搞清楚。所以花时间好好补补课。

概念

本质上自动装箱、拆箱只是Java语言中的语法糖,在Java里使用特别广泛。我们通过一个示例看一下这些语法糖在编译后发生的变化。

    public static void main(String[] args) {
        Integer a = 1;//自动装箱
        int b = a;//自动拆箱
    }

反编译class文件后如下:

  public static void main(java.lang.String[]);
    Code:
       0: iconst_1
       1: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
       4: astore_1
       5: aload_1
       6: invokevirtual #3                  // Method java/lang/Integer.intValue:()I
       9: istore_2
      10: return

我们看到,在执行Integer a = 1;这句的时候,实际上执行的是Integer的静态方法valueOf():

Integer a = Integer.valueOf(1);

在执行int b = a;时,执行的是Integer的方法intValue():

int b = a.intValue();

这就是Java的自动状态、拆箱。

问题分析

我们看回示例代码的前两个打印:

Integer c = 3;
Integer d = 3;
Integer e = 321;
Integer f = 321;
System.out.println(c == d);//true
System.out.println(e == f);//false

what the fffffff...?都是对象进行比较,有毛的不一样?
这就是自动装箱中的一个陷阱了,我们根据字节码流程分析一下:
首先在上面的赋值操作中,我们知道是调用Integer类的valueOf(Object)方法生成Integer对象的:

Integer c = Integer.valueOf(3);

我们看一下这个valueOf()方法的实现:

    /**
     * Returns an {@code Integer} instance representing the specified
     * {@code int} value.  If a new {@code Integer} instance is not
     * required, this method should generally be used in preference to
     * the constructor {@link #Integer(int)}, as this method is likely
     * to yield significantly better space and time performance by
     * caching frequently requested values.
     *
     * This method will always cache values in the range -128 to 127,
     * inclusive, and may cache other values outside of this range.
     *
     * @param  i an {@code int} value.
     * @return an {@code Integer} instance representing {@code i}.
     * @since  1.5
     */
    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

其实看注释我们也能看到,这个方法会缓存范围为-128到127的值,也就是如果i的值在此范围,将会从定义好的Integer对象数组中返回Integer对象,这样做可以减少对象的频繁创建。
IntegerCache是Integer中的私有静态内部类,我们看一下是怎么定义的:

    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() {}
    }

可以清楚的看到Integer缓存数组cache[]的创建过程,预先创建并添加了-128到127范围内的Integer对象。因为3在缓存值范围内,c和d拿到的是缓存中相同的对象,而e和f因为值超出了范围,各自拿到的是新创建的对象,对象之间的“==”号比较的是地址,自然一个是true,一个是false。
下面分析第三四句打印:

Integer a = 1;
Integer b = 2;
Integer c = 3;
System.out.println(c == (a + b));//true
System.out.println(c.equals(a + b));//true

虽然这两句打印结果相同,但是操作方式是不一样的。我们看这两句打印的反编译class指令:

Code:
      80: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
      83: aload_3
      84: invokevirtual #8                  // Method java/lang/Integer.intValue:()I
      87: aload_1
      88: invokevirtual #8                  // Method java/lang/Integer.intValue:()I
      91: aload_2
      92: invokevirtual #8                  // Method java/lang/Integer.intValue:()I
      95: iadd
      96: if_icmpne     103
      99: iconst_1
     100: goto          104
     103: iconst_0
     104: invokevirtual #7                  // Method java/io/PrintStream.println:(Z)V
     107: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
     110: aload_3
     111: aload_1
     112: invokevirtual #8                  // Method java/lang/Integer.intValue:()I
     115: aload_2
     116: invokevirtual #8                  // Method java/lang/Integer.intValue:()I
     119: iadd
     120: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
     123: invokevirtual #9                  // Method java/lang/Integer.equals:(Ljava/lang/Object;)Z
     126: invokevirtual #7                  // Method java/io/PrintStream.println:(Z)V

第一句打印,根据84、88、92、95行我们看到,a、b、c都进行了拆箱操作,a和b拆箱后进行相加操作,跟拆箱后的c进行比较,结果自然是true。
第二句打印,根据112、116和119行,a和b分别拆箱然后相加,然后根据120行,相加后的值调用了Integer.valueOf()进行装箱。然后123行,c调用Integer的equals()方法进行比较。OK,那我们看一下这个方法的实现:

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

参数为object,如果类型同是Integer,则比较两者的基础值。否则返回false。
那第二句打印时true也就没有异议了。
下面分析打印第五六句:

Integer a = 1;
Integer b = 2;
Long g = 3L;
System.out.println(g == (a + b));//true
System.out.println(g.equals(a + b));//false

同样贴出两句打印语句的反编译指令:

     129: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
     132: aload         7
     134: invokevirtual #10                 // Method java/lang/Long.longValue:()J
     137: aload_1
     138: invokevirtual #8                  // Method java/lang/Integer.intValue:()I
     141: aload_2
     142: invokevirtual #8                  // Method java/lang/Integer.intValue:()I
     145: iadd
     146: i2l
     147: lcmp
     148: ifne          155
     151: iconst_1
     152: goto          156
     155: iconst_0
     156: invokevirtual #7                  // Method java/io/PrintStream.println:(Z)V
     159: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
     162: aload         7
     164: aload_1
     165: invokevirtual #8                  // Method java/lang/Integer.intValue:()I
     168: aload_2
     169: invokevirtual #8                  // Method java/lang/Integer.intValue:()I
     172: iadd
     173: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
     176: invokevirtual #11                 // Method java/lang/Long.equals:(Ljava/lang/Object;)Z
     179: invokevirtual #7                  // Method java/io/PrintStream.println:(Z)V

第一句打印,根据134、138、142、145,g拆箱后入栈,然后a和b拆箱进行相加,然后跟g进行比较,值相同,结果为true。
第二句打印,a和b拆箱后相加,然后对结果3进行装箱,然后Long类型的g调用Long.equals(Object obj)方法与该结果进行比较,我们看一下Long.equals()方法的实现:

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

实现方式跟Integer如出一辙,如果比较对象是Long类型,则对基础值进行比较,否则返回false。这里因为结果3是Integer类型,所以是false。
分析完了上面那些,不知大家是否和我一样还有一些疑问?比如下面:

Integer c = 3;
int h = 3;
System.out.println(c == h);
System.out.println(c.equals(h));

我们可能都知道这两个打印结果都是true,但是他们背后又是怎么实现的呢?c和h比较,到底是装箱后的equals比较,还是拆箱后的值比较呢?Integer.equals(Object)这个方法参数类型是object,而我们传入的是基本数据类型int,这又是怎么处理的呢?
篇幅原因这里就不贴反编译码了,直接给出结论:第一句打印中,是对c拆箱后比较的。第二句中,会对h进行自动状态,然后通过equals(Object)方法比较。

总结

根据以上分析,我们可以得出一些结论:

  1. 如果是包装类之间的“==”运算,在没有算数运算的情况下不会自动拆箱,比较的是对象地址。遇到算数运算时会进行自动拆箱,比较数值。
  2. 如果是包装类和基本类型之间的"=="比较,包装类会拆箱后比较
  3. 包装类的equals(Object)方法是不处理数据转型关系的,如Long.equals(Integer.value(3)),类型不一致就会返回false。

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