美文网首页
详解Java中自动装箱拆箱

详解Java中自动装箱拆箱

作者: 千雨茶 | 来源:发表于2020-07-26 11:34 被阅读0次

    一段代码引发的问题

    最近在学习一本关于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中自动装箱拆箱

          本文链接:https://www.haomeiwen.com/subject/nhjwlktx.html