Java 语法糖(一): 自动拆箱

正文

大家在日常开发中很可能使用过 java 的自动拆箱(unboxing)操作。
我举个自动拆箱的简单例子

public class Main {
  private int addOne(Integer in) {
    return in + 1;
  }
}

在上述代码中编译器会自动加上将 Integer 转化为 int 的操作(可以把return in + 1 想象成 return in.intValue() + 1)。

那么为何 java 的编译器会支持自动拆箱操作呢?

java 语言规范的 5.1.8 中,可以看到如下描述

Unboxing conversion converts expressions of reference type to corresponding expressions of primitive type. Specifically, the following eight conversions are called the unboxing conversions:

  • From type Boolean to type boolean
  • From type Byte to type byte
  • From type Short to type short
  • From type Character to type char
  • From type Integer to type int
  • From type Long to type long
  • From type Float to type float
  • From type Double to type double

At run time, unboxing conversion proceeds as follows:

  • If r is a reference of type Boolean, then unboxing conversion converts r into r.booleanValue()
  • If r is a reference of type Byte, then unboxing conversion converts r into r.byteValue()
  • If r is a reference of type Character, then unboxing conversion converts r into r.charValue()
  • If r is a reference of type Short, then unboxing conversion converts r into r.shortValue()
  • If r is a reference of type Integer, then unboxing conversion converts r into r.intValue()
  • If r is a reference of type Long, then unboxing conversion converts r into r.longValue()
  • If r is a reference of type Float, unboxing conversion converts r into r.floatValue()
  • If r is a reference of type Double, then unboxing conversion converts r into r.doubleValue()
  • If r is null, unboxing conversion throws a NullPointerException

A type is said to be convertible to a numeric type if it is a numeric type (§4.2), or it is a reference type that may be converted to a numeric type by unboxing conversion.

A type is said to be convertible to an integral type if it is an integral type, or it is a reference type that may be converted to an integral type by unboxing conversion.

因为自动拆箱是 java 语言规范中指定的功能,所以编译器自然需要实现它。

我们写个简单的程序,来看看编译器实际上做了些什么。

public class Main {
    private int f(Integer in) {
        return in;
    }
}

执行如下命令对 Main.java 进行编译

javac Main.java

执行如下命令可以查看字节码文件中的内容

javap -cp . -p -v 'Main'

完整的内容较长,和 f(...) 直接相关的部分如下

  private int f(java.lang.Integer);
    descriptor: (Ljava/lang/Integer;)I
    flags: ACC_PRIVATE
    Code:
      stack=1, locals=2, args_size=2
         0: aload_1
         1: invokevirtual #2                  // Method java/lang/Integer.intValue:()I
         4: ireturn
      LineNumberTable:
        line 3: 0

f(...) 对应的字节码指令有 3 条, 作用如下

  1. aload_1, 其作用是将 1slot 中的值加载到操作数栈中(f(...)方法只有一个参数 in1slot 中保存的就是 in 这个参数的引用,细节这里就不展开了)
  2. invokevirtual #2, 调用常量池中与 #2 对应的方法, 也就是java/lang/Integer.intValue:()I 这个方法(其实就是对 in 参数调用 java/lang/Integer.intValue:()I 方法)
  3. ireturn, 将操作数栈栈顶的int值作为 f(...) 的返回值(操作数栈栈顶的值就是 in 参数调用 java/lang/Integer.intValue:()I 方法后的返回值)

显式地进行拆箱

大致可以猜到,编译器在遇到需要对 Integer 类型进行拆箱的场景时,会自动调用 Integer 类中的 intValue() 方法

我们可以写一个 Temp.java 来验证一下

public class Temp {
    private int f(Integer in) {
        return in.intValue();
    }
}

执行如下命令就能看到 Temp.java 中的 f(...) 对应的字节码指令

javac Temp.java
javap -cp . -p -v 'Temp'

Temp.java 中的 f(...) 的字节码指令展示如下

  private int f(java.lang.Integer);
    descriptor: (Ljava/lang/Integer;)I
    flags: ACC_PRIVATE
    Code:
      stack=1, locals=2, args_size=2
         0: aload_1
         1: invokevirtual #2                  // Method java/lang/Integer.intValue:()I
         4: ireturn
      LineNumberTable:
        line 3: 0

可见 Main.java Temp.java 中的 f(...) 在编译后生成的字节码指令确实是相同的

也可以用 cfr 这个工具验证一下。
cfr-0.148.jar 为例,执行如下命令,就能看到显式的拆箱操作

java -jar cfr-0.148.jar Main --sugarboxing false 

运行结果如下

/*
 * Decompiled with CFR 0.148.
 */
public class Main {
    private int f(Integer n) {
        return n.intValue();
    }
}

可见,cfr 运行的结果和我们之前的猜测是一致的。

参考文章

  1. https://www.benf.org/other/cfr/boxing-isnt-magic.html
  2. Java Language Specification 的 5.1.8 小节
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。