JVM第五卷---编译期处理

JVM第五卷---编译期处理


编译期处理

所谓的 语法糖 ,其实就是指 java 编译器把 *.java 源码编译为 *.class 字节码的过程中,自动生成
和转换的一些代码,主要是为了减轻程序员的负担,算是 java 编译器给我们的一个额外福利(给糖吃
嘛)

注意,以下代码的分析,借助了 javap 工具,idea 的反编译功能,idea 插件 jclasslib 等工具。

另外,编译器转换的结果直接就是 class 字节码,只是为了便于阅读,给出了 几乎等价 的 java 源码方式,并不是编译器还会转换出中间的 java 源码,切记。

默认构造器

public class Candy1 { }

编译成class后的代码

JVM第五卷---编译期处理


自动拆装箱

这个特性是 JDK 5 开始加入的, 代码片段1 :

JVM第五卷---编译期处理

这段代码在 JDK 5 之前是无法编译通过的,必须改写为 代码片段2 :

JVM第五卷---编译期处理

显然之前版本的代码太麻烦了,需要在基本类型和包装类型之间来回转换(尤其是集合类中操作的都是包装类型),因此这些转换的事情在 JDK 5 以后都由编译器在编译阶段完成。即 代码片段1 都会在编译阶段被转换为 代码片段2


泛型集合取值–泛型擦除

泛型也是在 JDK 5 开始加入的特性,但 java 在编译泛型代码后会执行 泛型擦除 的动作,即泛型信息在编译为字节码之后就丢失了,实际的类型都当做了 Object 类型来处理:

JVM第五卷---编译期处理
所以在取值时,编译器真正生成的字节码中,还要额外做一个类型转换的操作:

JVM第五卷---编译期处理
如果前面的 x 变量类型修改为 int 基本类型那么最终生成的字节码是:

JVM第五卷---编译期处理
还好这些麻烦事都不用自己做。

擦除的是字节码上的泛型信息,可以看到 LocalVariableTypeTable 仍然保留了方法参数泛型的信息

JVM第五卷---编译期处理
JVM第五卷---编译期处理

JVM第五卷---编译期处理

局部变量没有办法通过反射的方式,拿到泛型信息,只有在方法的参数和返回值上带的泛型信息才可以通过反射获取到

使用反射,仍然能够获得这些信息:

JVM第五卷---编译期处理

JVM第五卷---编译期处理
输出
JVM第五卷---编译期处理


可变参数

可变参数也是 JDK 5 开始加入的新特性:

例如:

JVM第五卷---编译期处理
可变参数 String… args 其实是一个 String[] args ,从代码中的赋值语句中就可以看出来。同样 java 编译器会在编译期间将上述代码变换为:

JVM第五卷---编译期处理
注意

如果调用了 foo() 则等价代码为 foo(new String[]{})创建了一个空的数组,而不会传递null 进去


foreach 循环

仍是 JDK 5 开始引入的语法糖,数组的循环:

JVM第五卷---编译期处理
会被编译器转换为:

JVM第五卷---编译期处理
而集合的循环:
JVM第五卷---编译期处理
实际被编译器转换为对迭代器的调用:
JVM第五卷---编译期处理
注意

foreach 循环写法,能够配合数组,以及所有实现了 Iterable 接口的集合类一起使用,其中Iterable 用来获取集合的迭代器( Iterator )


switch 字符串

从 JDK 7 开始,switch 可以作用于字符串和枚举类,这个功能其实也是语法糖,例如:

JVM第五卷---编译期处理
注意

switch 配合 String 和枚举使用时,变量不能为null,原因分析完语法糖转换后的代码应当自然清楚

会被编译器转换为:

JVM第五卷---编译期处理
可以看到,执行了两遍 switch,第一遍是根据字符串的 hashCode 和 equals 将字符串的转换为相应byte 类型,第二遍才是利用 byte 执行进行比较。

为什么第一遍时必须既比较 hashCode,又利用 equals 比较呢

hashCode 是为了提高效率,减少可能的比较;而 equals 是为了防止 hashCode 冲突例如 BM 和 C. 这两个字符串的hashCode值都是2123 ,如果有如下代码:

JVM第五卷---编译期处理
会被编译器转换为:

JVM第五卷---编译期处理


switch 枚举

switch 枚举的例子,原始代码:

JVM第五卷---编译期处理
转换后代码:

JVM第五卷---编译期处理


枚举类

JDK 7 新增了枚举类,以前面的性别枚举为例:

enum Sex { MALE, FEMALE }

转换后代码:

JVM第五卷---编译期处理


try-with-resources

JDK 7 开始新增了对需要关闭的资源处理的特殊语法 try-with-resources:`

JVM第五卷---编译期处理
其中资源对象需要实现 AutoCloseable 接口,例如 InputStream 、 OutputStream 、 Connection 、 Statement 、 ResultSet 等接口都实现了 AutoCloseable ,使用 try-with- resources 可以不用写 finally 语句块,编译器会帮助生成关闭资源代码,例如:

JVM第五卷---编译期处理
会被转换为:

JVM第五卷---编译期处理
为什么要设计一个 addSuppressed(Throwable e) (添加被压制异常)的方法呢?是为了防止异常信息的丢失(想想 try-with-resources 生成的 fianlly 中如果抛出了异常):

JVM第五卷---编译期处理
输出:

JVM第五卷---编译期处理
如以上代码所示,两个异常信息都不会丢


方法重写时的桥接方法

我们都知道,方法重写时对返回值分两种情况:

  • 父子类的返回值完全一致
  • 子类返回值可以是父类返回值的子类(比较绕口,见下面的例子)

JVM第五卷---编译期处理
对于子类,java 编译器会做如下处理:

JVM第五卷---编译期处理
其中桥接方法比较特殊,仅对 java 虚拟机可见,并且与原来的 public Integer m() 没有命名冲突,可以用下面反射代码来验证:

for (Method m : B.class.getDeclaredMethods()) 
{ System.out.println(m); }

会输出:

public java.lang.Integer test.candy.B.m() 
public java.lang.Number test.candy.B.m()

匿名内部类

源代码:

JVM第五卷---编译期处理
转换后代码:

JVM第五卷---编译期处理
JVM第五卷---编译期处理
引用局部变量的匿名内部类,源代码:

JVM第五卷---编译期处理
转换后代码:
JVM第五卷---编译期处理
注意

这同时解释了为什么匿名内部类引用局部变量时,局部变量必须是 final 的:因为在创建Candy11$1 对象时,将 x 的值赋值给了 Candy11$1 对象的 val$x 属性,所以 x 不应该再发生变化了,如果变化,那么 val$x 属性没有机会再跟着一起变化

上一篇:011 JVM中垃圾收集算法


下一篇:JVM内存区域划分