JVM第五卷---编译期处理
- 编译期处理
-
- 默认构造器
- 自动拆装箱
- 泛型集合取值--泛型擦除
- 可变参数
- foreach 循环
- switch 字符串
- switch 枚举
- 枚举类
- try-with-resources
- 方法重写时的桥接方法
- 匿名内部类
- 插入式注解处理器
编译期处理
所谓的 语法糖 ,其实就是指 java 编译器把 *.java 源码编译为 *.class 字节码的过程中,自动生成
和转换的一些代码,主要是为了减轻程序员的负担,算是 java 编译器给我们的一个额外福利(给糖吃
嘛)
注意,以下代码的分析,借助了 javap 工具,idea 的反编译功能,idea 插件 jclasslib 等工具。
另外,编译器转换的结果直接就是 class 字节码,只是为了便于阅读,给出了 几乎等价 的 java 源码方式,并不是编译器还会转换出中间的 java 源码,切记。
默认构造器
public class Candy1 { }
编译成class后的代码
自动拆装箱
这个特性是 JDK 5 开始加入的, 代码片段1 :
这段代码在 JDK 5 之前是无法编译通过的,必须改写为 代码片段2 :
显然之前版本的代码太麻烦了,需要在基本类型和包装类型之间来回转换(尤其是集合类中操作的都是包装类型),因此这些转换的事情在 JDK 5 以后都由编译器在编译阶段完成。即 代码片段1 都会在编译阶段被转换为 代码片段2
泛型集合取值–泛型擦除
泛型也是在 JDK 5 开始加入的特性,但 java 在编译泛型代码后会执行 泛型擦除
的动作,即泛型信息在编译为字节码之后就丢失了,实际的类型都当做了 Object 类型来处理:
所以在取值时,编译器真正生成的字节码中,还要额外做一个类型转换的操作:
如果前面的 x 变量类型修改为 int 基本类型那么最终生成的字节码是:
还好这些麻烦事都不用自己做。
擦除的是字节码上的泛型信息
,可以看到 LocalVariableTypeTable
仍然保留了方法参数泛型的信息
局部变量没有办法通过反射的方式,拿到泛型信息,只有在方法的参数和返回值上带的泛型信息才可以通过反射获取到
使用反射,仍然能够获得这些信息:
输出
可变参数
可变参数也是 JDK 5 开始加入的新特性:
例如:
可变参数 String… args 其实是一个 String[] args ,从代码中的赋值语句中就可以看出来。同样 java 编译器会在编译期间将上述代码变换为:
注意
如果调用了 foo() 则等价代码为 foo(new String[]{})
,创建了一个空的数组,而不会传递null 进去
foreach 循环
仍是 JDK 5 开始引入的语法糖,数组的循环:
会被编译器转换为:
而集合的循环:
实际被编译器转换为对迭代器的调用:
注意
foreach 循环写法,能够配合数组,以及所有实现了 Iterable 接口的集合类一起使用,其中Iterable 用来获取集合的迭代器( Iterator )
switch 字符串
从 JDK 7 开始,switch 可以作用于字符串和枚举类,这个功能其实也是语法糖,例如:
注意
switch 配合 String 和枚举使用时,变量不能为null,原因分析完语法糖转换后的代码应当自然清楚
会被编译器转换为:
可以看到,执行了两遍 switch,第一遍是根据字符串的 hashCode 和 equals 将字符串的转换为相应byte 类型,第二遍才是利用 byte 执行进行比较。
为什么第一遍时必须既比较 hashCode,又利用 equals 比较呢
?
hashCode 是为了提高效率,减少可能的比较;而 equals 是为了防止 hashCode 冲突
,例如 BM 和 C. 这两个字符串的hashCode值都是2123 ,如果有如下代码:
会被编译器转换为:
switch 枚举
switch 枚举的例子,原始代码:
转换后代码:
枚举类
JDK 7 新增了枚举类,以前面的性别枚举为例:
enum Sex { MALE, FEMALE }
转换后代码:
try-with-resources
JDK 7 开始新增了对需要关闭的资源处理的特殊语法 try-with-resources
:`
其中资源对象需要实现 AutoCloseable
接口,例如 InputStream 、 OutputStream 、 Connection 、 Statement 、 ResultSet
等接口都实现了 AutoCloseable
,使用 try-with- resources
可以不用写 finally
语句块,编译器会帮助生成关闭资源代码,例如:
会被转换为:
为什么要设计一个 addSuppressed(Throwable e) (添加被压制异常
)的方法呢?是为了防止异常信息的丢失(想想 try-with-resources 生成的 fianlly 中如果抛出了异常):
输出:
如以上代码所示,两个异常信息都不会丢
方法重写时的桥接方法
我们都知道,方法重写时对返回值分两种情况:
- 父子类的返回值完全一致
- 子类返回值可以是父类返回值的子类(比较绕口,见下面的例子)
对于子类,java 编译器会做如下处理:
其中桥接方法比较特殊,仅对 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()
匿名内部类
源代码:
转换后代码:
引用局部变量的匿名内部类,源代码:
转换后代码:
注意
这同时解释了为什么匿名内部类引用局部变量时,局部变量必须是 final 的:因为在创建Candy11$1 对象时,将 x 的值赋值给了 Candy11$1 对象的 val$x 属性,所以 x 不应该再发生变化了,如果变化,那么 val$x 属性没有机会再跟着一起变化
插入式注解处理器
插入式注解处理器可以看做是一组编译器插件,当这些插件工作的时候,可以读取,修改和添加抽象语法树中任意的元素。
如果这些插件在处理注解期间对语法树进行过修改,编译器将会到解析及填充符号表的过程重新处理,直到所有插入式注解处理器都没有再对语法树进行修改为止,每一次循环称为一个轮次。
著名的Lombok就是通过插入式注解处理器实现的。
打包自定义插入式注解: https://www.cnblogs.com/avenwu/p/4173899.html
获取类、字段:https://blog.csdn.net/zhuhai__yizhi/article/details/51394810
编辑语法树:https://blog.csdn.net/a_zhenzhen/article/details/86065063
https://www.cnblogs.com/kanyun/p/11541826.html
生成 GET / SET 方法:https://www.jianshu.com/p/68fcbc154c2f
插入式注解-自动生成 Get / Set