Java语法和Java字节码之间的差异,都是通过Java编译器来协调的。
自动装箱与自动拆箱
自动装箱(auto-boxing)与自动拆箱(auto-unboxing)。
Java语言拥有8个基本类型,每个基本类型都有对应的包装(wrapper)类型。之所以需要包装类型,是因为许多Java核心类库的API都是面向对象的。如Java核心类库中的容器类,就只支持引用类型。
当需要一个能够存储数值的容器类时,往往定义一个存储包装类对象的容器。对于基本类型的数值来说,需要先将其转换为对应的包装类,再存图容器之中。在Java程序中,这个转换可以是显式,也可以是隐式的,后者正是Java中的自动装箱。
1 | public int foo() { |
上述Java代码中,构造了一个Integer类型的ArrayList,并且向其中添加一个int值0。然后获取该ArrayList的第0个元素,并作为int值返回给调用者。
1 | public int foo(); |
当向泛型参数为Integer的ArrayList添加int值时,便需要用到自动装箱了。在上述字节码偏移量为10的指令中,调用了Integer.valueOf方法,将int类型的值转换为Integer类型,再存储至容器类中。
1 | public static Integer valueOf(int i) { |
Integer.valueOf源代码,当请求的int值再某个范围内时,会返回缓存了的Integer对象;而当所请求的int值再范围之外时,则会新建一个Integer对象。
配置参数java.lang.Integer.IntegerCache.high,扩大Integer缓存的范围。JVM参数-XX:+AggressiveOpts也会将IntegerCache.high调整至20000。
但是Java并不支持对IntegerCache.low的更改,对于小于-128的整数,无法直接使用由Java核心类库所缓存的Integer对象。
当从泛型参数为Integer的ArrayList取出元素时,得到的实际上也是Integer对象。如果应用程序期待的是一个int值,那么就会发生自动拆箱。
自动拆箱对应的是字节码偏移量为25的指令。该指令将调用Integer.intValue方法。这是一个实例方法,直接返回Integer对象所存储的int值。
泛型与类型擦除
示例中生成的字节码,往ArrayList中添加元素的add方法,所接受的参数类型是Object;而从ArrayList中获取元素的get方法,其返回类型也是Object。
后者返回类型,在字节码中需要进行向下转换,将所返回的Object强制转换为Integer,方能进行接下来的自动拆箱。
之所以出现这种情况,是因为Java泛型的类型擦除:Java程序里的泛型信息,在JVM里全部都丢失了,这么做是为了兼容引入泛型之前的代码。
但并不是每一个泛型参数被擦除类型后都会变成Object类。对于限定了继承类的泛型参数,经过类型擦除后,所有的泛型类型都将变成所限定的继承类。Java编译器将选取该泛型所能指代的所有类中层次最高的那个,作为替换泛型的类。
1 | //定义了一个T extends Number的泛型参数。 |
1 | T foo(T); |
foo方法的方法描述符所接收参数的类型以及返回类型都为Number。方法描述符是JVM识别方法调用的目标方法的关键。
字节码中仍存在泛型参数的信息,方法声明里的T foo(T),以及方法签名(Signature)中的”(TT;)TT;”。这类信息主要由Java编译器在编译他类时使用。
泛型会被类型擦除,但依然必要。Java编译器可以根据泛型参数判断程序中的语法是否正确。尽管经过类型擦除后,ArrayList.add方法所接收的参数是Object类型,但是往泛型参数为Integer类型的ArrayList中添加字符串对象,Java编译器是会报错的。
桥接方法
参数类型不同
泛型的类型擦除带来了不少问题。其中一个是方法重写。但通过桥接方法可以解决参数类型不匹配问题。
1 | class Merchant<T extends Customer> { |
1 | class VIPOnlyMerchant extends Merchant<VIP> |
VIPOnlyMerchant中的actionPrice方法经过类型擦除后,父类的方法描述符为(LCustomer;)D,而子类的方法描述符为(LVIP;)D。这显然不符合JVM关于方法重写的定义。
为了保证编译而成的Java字节码能够保留重写的语义,Java编译器额外添加了一个桥接方法。该桥接方法在字节码层面重写了父类的方法,并将调用子类的方法。
VIPOnlyMerchant类包含一个桥接方法actionPrice(Customer),它重写了父类的同名同方法描述符的方法。该桥接方法将传入的Customer参数强制转换为VIP类型,再调用原本的actionPrice(VIP)方法。
当一个声明类型为Merchant,实际类型为VIPOnlyMerchant的对象,调用actionPrice方法时,字节码里的符号引用指向的是Merchant.actionPrice(Customer)方法。JVM将动态绑定至VIPOnlyMerchant类的桥接方法之中,并且调用其actionPrice(VIP)方法。
在Javap的输出中,该桥接方法的访问标识符除了代表桥接方法的ACC_BRIDGE之外,还有ACC_SYNTHETIC。它表示该方法对于Java源代码来说是不可见的。当尝试通过传入一个声明类型为Customer的对象作为参数,调用VIPOnlyMerchant类的actionPrice方法时,Java编译器会报错,并且提示参数类型不匹配。
Customer customer = new VIP();
new VIPOnlyMerchant().actionPrice(customer); // 编译出错
返回类型不同
如果子类定义了一个与父类参数类型相同的方法,其返回类型为父类方法返回类型的子类,那么Java编译器也会为其生成桥接方法。
1 | class Merchant { |
1 | class NaiveMerchant extends Merchant |
该桥接方法标注了ACC_SYNTHETIC不可见,当在Java程序中调用NativeMerchant.actionPrice时,只会调用到原方法。
class文件里允许出现两个同名、同参数类型但是不同返回类型的方法。原方法与桥接方法便是例子。
其他语法糖
变长参数
try-with-resources以及在同一catch代码块中捕获多种异常等语法糖。
foreach循环允许Java程序在for循环里遍历数组或者Iterable对象。
foreach循环数组
对于数组来说,foreach循环将从0开始逐一访问数组中的元素,直至数组的末尾。
1 | public void foo(int[] array) { |
foreach循环Iterator
对于Iterable对象来说,foreach循环将调用其iterator方法,并且用它的hasNext以及next方法来遍历该Iterable对象中的元素。
1 | public void foo(ArrayList<Integer> list) { |
字符串switch
字符串switch编译而成的字节码看起来非常复杂,但实际上就是一个哈希桶。由于每个case所截获的字符串都是常量值,因此,Java编译器会将原来的字符串switch转换为int值switch,比较所输入的字符串的哈希值。
由于字符串哈希值很容易发生碰撞,因此,还需要用String.equals逐个比较相同哈希值的字符串。
总结
基本类型和其包装类型之间的自动转换,也就是自动装箱、自动拆箱,是通过加入[Wrapper].valueOf(如Integer.valueOf)以及[Wrapper].[primitive]Value(如Integer.intValue)方法调用来实现的。
Java程序中的泛型信息会被擦除。Java编译器将选取该泛型所能指代的所有类中层次最高的那个,作为替换泛型的具体类。
由于Java语义与Java字节码中关于重写的定义并不一致,因此Java编译器会生成桥接方法作为适配器。