正文
大家在日常开发中很可能使用过 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
Booleanto typeboolean- From type
Byteto typebyte- From type
Shortto typeshort- From type
Characterto typechar- From type
Integerto typeint- From type
Longto typelong- From type
Floatto typefloat- From type
Doubleto typedoubleAt run time, unboxing conversion proceeds as follows:
- If
ris a reference of typeBoolean, then unboxing conversion convertsrintor.booleanValue()- If
ris a reference of typeByte, then unboxing conversion convertsrintor.byteValue()- If
ris a reference of typeCharacter, then unboxing conversion convertsrintor.charValue()- If
ris a reference of typeShort, then unboxing conversion convertsrintor.shortValue()- If
ris a reference of typeInteger, then unboxing conversion convertsrintor.intValue()- If
ris a reference of typeLong, then unboxing conversion convertsrintor.longValue()- If
ris a reference of typeFloat, unboxing conversion convertsrintor.floatValue()- If
ris a reference of typeDouble, then unboxing conversion convertsrintor.doubleValue()- If
risnull, unboxing conversion throws aNullPointerExceptionA 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 条, 作用如下
-
aload_1, 其作用是将1号slot中的值加载到操作数栈中(f(...)方法只有一个参数in,1号slot中保存的就是in这个参数的引用,细节这里就不展开了) -
invokevirtual #2, 调用常量池中与#2对应的方法, 也就是java/lang/Integer.intValue:()I这个方法(其实就是对in参数调用java/lang/Integer.intValue:()I方法) -
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 运行的结果和我们之前的猜测是一致的。