《深入拆解Java虚拟机》学习笔记 Day15 -- Java语法糖

121 阅读3分钟

之前的知识背景

方法描述符 【方法名 + 入参+出参】

它是由方法的参数类型以及返回类型所构成。在同一个类中,如果同时出现多个名字相同且描述符也相同的方法,那么 Java 虚拟机会在类的验证阶段报错。

Java 虚拟机中关于方法重写的判定同样基于方法描述符。也就是说,如果子类定义了与父类中非私有、非静态方法同名的方法,那么只有当这两个方法的参数类型以及返回类型一致,Java 虚拟机才会判定为重写。

自动装箱与自动拆箱

自动装箱(auto-boxing)和自动拆箱(auto-unboxing)

指的是Java的8种基本类型和其包装类型的转换,因为许多 Java 核心类库的 API 都是面向对象的,基本类型没法用。 例如,Java 核心类库中的容器类(List等),就只支持引用类型。

泛型与类型擦除

Java 程序里的泛型信息,在 Java 虚拟机里全部都丢失了。这么做主要是为了兼容引入泛型之前的代码。

类型擦除成什么

是不是所有的类型都会被擦成Object呢,答案是否定的。

对于限定了继承类的泛型参数,经过类型擦除后,所有的泛型参数都将变成所限定的继承类

class GenericTest<T extends Number> {
  T foo(T t) {
    return t;
  }
}

foo 方法的方法描述符所接收参数的类型以及返回类型都为 Number。

桥接方法 解决泛型擦除的问题

class Merchant<T extends Customer> {
  public double actionPrice(T customer) {
    return 0.0d;
  }
}

class VIPOnlyMerchant extends Merchant<VIP> {
  @Override
  public double actionPrice(VIP customer) {
    return 0.0d;
  }
}

上面的代码,从Java规则层面是没问题的,但是经过类型擦除后,父类的方法描述符为 (LCustomer;)D,而子类的方法描述符为 (LVIP;)D。这显然不符合 Java 虚拟机关于方法重写的定义。

为了保证编译而成的 Java 字节码能够保留重写的语义,Java 编译器额外添加了一个桥接方法。该桥接方法在字节码层面重写了父类的方法,并将调用子类的方法。


class VIPOnlyMerchant extends Merchant<VIP>
...
  public double actionPrice(VIP);
    descriptor: (LVIP;)D
    flags: (0x0001) ACC_PUBLIC
    Code:
         0: dconst_0
         1: dreturn

  public double actionPrice(Customer);
    descriptor: (LCustomer;)D
    flags: (0x1041) ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
    Code:
         0: aload_0
         1: aload_1
         2: checkcast class VIP
         5: invokevirtual actionPrice:(LVIP;)D
         8: dreturn

// 这个桥接方法等同于
public double actionPrice(Customer customer) {
  return actionPrice((VIP) customer);
}

VIPOnlyMerchant 类将包含一个桥接方法 actionPrice(Customer),它重写了父类的同名同方法描述符的方法。该桥接方法将传入的 Customer 参数强制转换为 VIP 类型,再调用原本的 actionPrice(VIP) 方法。

当一个声明类型为 Merchant,实际类型为 VIPOnlyMerchant 的对象,调用 actionPrice 方法时,字节码里的符号引用指向的是 Merchant.actionPrice(Customer) 方法。Java 虚拟机将动态绑定至 VIPOnlyMerchant 类的桥接方法之中,并且调用其 actionPrice(VIP) 方法。

参考文章 :15 | Java语法糖与Java编译器 (geekbang.org)