三、编译期处理
所谓的 语法糖 ,其实就是指 java 编译器把* .java 源码编译为* .class 字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担,算是 java 编译器给我们的一个额外福利
【注意】以下代码的分析,借助了 javap 工具,idea 的反编译功能,idea 插件 jclasslib 等工具。另外, 编译器转换的结果直接就是 class 字节码,只是为了便于阅读,给出了 几乎等价 的 java 源码方式,并不是编译器还会转换出中间的 java 源码,切记。
① 语法糖——默认构造函数
public class Candy1 {
}
经过编译期优化后
public class Candy1 {
//这个无参构造器是java编译器帮我们加上的
public Candy1() {
//即调用父类 Object 的无参构造方法,即调用 java/lang/Object." <init>":()V
super();
}
}
② 语法糖——自动拆装箱
基本类型和其包装类型的相互转换过程,称为拆装箱
在JDK 5以后,它们的转换可以在编译期自动完成
public class Demo2 {
public static void main(String[] args) {
Integer x = 1;
int y = x;
}
}
转换过程如下
public class Demo2 {
public static void main(String[] args) {
//基本类型赋值给包装类型,称为装箱
Integer x = Integer.valueOf(1);
//包装类型赋值给基本类型,称谓拆箱
int y = x.intValue();
}
}
显然在旧版本中需要在基本类型和包装类型之间来回转换【尤其是集合类中操作的都是包装类型】,因此这些转换的事情在JDK5以后都由编译器在编译阶段完成。即代码片段1都会在编译阶段被转化代码片段2
③ 语法糖——泛型集合取值
泛型也是在 JDK 5 开始加入的特性,但 java 在编译泛型代码后会执行 泛型擦除 的动作,即泛型信息在编译为字节码之后就丢失了(不区分泛型,统一视为Object对待),实际的类型都当做了 Object 类型来处理:
public class Demo3 {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(10);
Integer x = list.get(0);
}
}
对应字节码
Code:
stack=2, locals=3, args_size=1
0: new #2 // class java/util/ArrayList
3: dup
4: invokespecial #3 // Method java/util/ArrayList."<init>":()V
7: astore_1
8: aload_1
9: bipush 10
11: invokestatic #4 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
//这里进行了泛型擦除,实际调用的是add(Objcet o)
14: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
19: pop
20: aload_1
21: iconst_0
//这里也进行了泛型擦除,实际调用的是get(Object o)
22: invokeinterface #6, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
//这里进行了类型转换,将Object转换成了Integer
27: checkcast #7 // class java/lang/Integer
30: astore_2
31: return
所以调用get函数取值时,有一个类型转换的操作
Integer x = (Integer) list.get(0);
如果要将返回结果赋值给一个int类型的变量,则还有自动拆箱的操作
int x = (Integer) list.get(0).intValue();
④ 语法糖——可变参数
可变参数也是JDK 5开始加入的新特性
public class Demo4 {
public static void foo(String... args) {
//将args赋值给arr,可以看出String...实际就是String[]
String[] arr = args;
System.out.println(arr.length);
}
public static void main(String[] args) {
foo("hello", "world");
}
}
可变参数 String… args 其实是一个 String[] args ,从代码中的赋值语句中就可以看出来。 同 样 java 编译器会在编译期间将上述代码变换为:
public class Demo4 {
public Demo4 {}
public static void foo(String[] args) {
String[] arr = args;
System.out.println(arr.length);
}
public static void main(String[] args) {
foo(new String[]{"hello", "world"});
}
}
注意,如果调用的是foo(),即未传递参数时,等价代码为foo(new String[]{}),创建了一个空数组,而不是直接传递的null
⑤ 语法糖——foreach
public class Demo5 {
public static void main(String[] args) {
//数组赋初值的简化写法也是一种语法糖。
int[] arr = {1, 2, 3, 4, 5};
for(int x : arr) {
System.out.println(x);
}
}
}
编译器会帮我们转换为
public class Demo5 {
public Demo5 {}
public static void main(String[] args) {
int[] arr = new int[]{1, 2, 3, 4, 5};
for(int i=0; i<arr.length; ++i) {
int x = arr[i];
System.out.println(x);
}
}
}
如果是集合使用foreach
public class Demo5 {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
for (Integer x : list) {
System.out.println(x);
}
}
}
集合要使用foreach,需要该集合类实现了Iterable接口,因为集合的遍历需要用到迭代器Iterator
public class Demo5 {
public Demo5 {}
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
//获得该集合的迭代器
Iterator<Integer> iterator = list.iterator();
while(iterator.hasNext()) {
Integer x = iterator.next();
System.out.println(x);
}
}
}
注意:foreach循环写法,能够配合数组,以及所有实现了Iterable接口的集合类一起使用,其中Iterable用来获取集合的迭代器(Iterable)
⑥ switch字符串
从JDK 7开始,switch可以作用于字符串和枚举型,这个功能其实也是语法糖
public class Demo6 {
public static void main(String[] args) {
String str = "hello";
switch (str) {
case "hello" :
System.out.println("h");
break;
case "world" :
System.out.println("w");
break;
default:
break;
}
}
}
注意:switch配合String和枚举使用时,变量不能为null
在编译器中执行的操作
public class Demo6 {
public Demo6() {
}
public static void main(String[] args) {
String str = "hello";
int x = -1;
//通过字符串的hashCode+value来判断是否匹配
switch (str.hashCode()) {
//hello的hashCode
case 99162322 :
//再次比较,因为字符串的hashCode有可能相等
if(str.equals("hello")) {
x = 0;
}
break;
//world的hashCode
case 11331880 :
if(str.equals("world")) {
x = 1;
}
break;
default:
break;
}
//用第二个switch在进行输出判断
switch (x) {
case 0:
System.out.println("h");
break;
case 1:
System.out.println("w");
break;
default:
break;
}
}
}
过程说明:
在编译期间,单个的switch被分为了两个
● 第一个用来匹配字符串,并给x赋值
——字符串的匹配用到了字符串的hashCode,还用到了equals方法
——使用hashCode是为了提高比较效率,使用equals是防止有hashCode冲突(如BM和C.这两个字符串的hashCode的值都为2123.)
● 第二个用来根据x的值来决定输出语句
在编译器中执行的操作
BM和C字符串比较在编译器中执行的操作
public class Demo6 {
public Demo6() {
}
public static void choose(String str) {
byte x = -1;
switch (str.hashCode()) {
case 2123 : // hashCode 值可能相同,需要进一步用equals比较
if(str.equals("C.")) {
x = 1;
} else if(str.equals("BM")) {
x = 0;
}
default:
switch (x) {
case 0:
System.out.println("h");
break;
case 1:
System.out.println("w");
break;
}
}
}
⑦ 语法糖——switch枚举(enum)
switch枚举的例子,原始代码:
enum SEX {
MALE, FEMALE;
}
public class Demo7 {
public static void main(String[] args) {
SEX sex = SEX.MALE;
switch (sex) {
case MALE:
System.out.println("man");
break;
case FEMALE:
System.out.println("woman");
break;
default:
break;
}
}
}
编译器中执行的代码如下
public class Demo7 {
/**
* 定义一个合成类(仅 jvm 使用,对我们不可见)
* 用来映射枚举的 ordinal 与数组元素的关系
* 枚举的 ordinal 表示枚举对象的序号,从 0 开始
* 即 MALE 的 ordinal()=0,FEMALE 的 ordinal()=1
*/
static class $MAP {
//数组大小即为枚举元素个数,里面存放了case用于比较的数字
static int[] map = new int[2];
static {
//ordinal即枚举元素对应所在的位置,MALE为0,FEMALE为1
map[SEX.MALE.ordinal()] = 1;
map[SEX.FEMALE.ordinal()] = 2;
}
}
public static void main(String[] args) {
SEX sex = SEX.MALE;
//将对应位置枚举元素的值赋给x,用于case操作
int x = $MAP.map[sex.ordinal()];
switch (x) {
case 1:
System.out.println("男");
break;
case 2:
System.out.println("女");
break;
default:
break;
}
}
}
enum SEX {
MALE, FEMALE;
}
⑧ 语法糖——枚举
JDK 7新增了枚举类,同样以性别的枚举为例:
enum SEX {
MALE, FEMALE; //与普通类不同,普通类实例对象为无穷多个【使用new关键字不断被创建】,
// 枚举类实例个数有限,如此性别例子中只有两个实例对象
}
转换后的代码
public final class Sex extends Enum<Sex> {
//对应枚举类中的元素
public static final Sex MALE;
public static final Sex FEMALE;
private static final Sex[] $VALUES;
static {
//调用构造函数,传入枚举元素的值及ordinal
MALE = new Sex("MALE", 0);
FEMALE = new Sex("FEMALE", 1);
$VALUES = new Sex[]{MALE, FEMALE};
}
//调用父类中的方法
private Sex(String name, int ordinal) {
super(name, ordinal);
}
public static Sex[] values() {
return $VALUES.clone();
}
public static Sex valueOf(String name) {
return Enum.valueOf(Sex.class, name);
}
}
⑨ 语法糖——twr1
JDK 7 开始新增了对需要关闭的资源的处理的特殊语法 try-with-resources
try(资源变量 = 创建资源对象) {
}catch (){
}
其中资源对象需要实现AutoCloseable接口,例如InputStream、OutputStream、 Connection、 Statement、ResultSet等接口都实现了AutoCloseable,使用try-with-resources 可以不用写finally语句块,编译器会帮助生成关闭资源代码,例如:
public class Demo {
public static void main(String[] args) {
try(InputStream is = new FileInputStream("d:\\data.txt")) {
System.out.println(is);
}catch (IOException e){
e.printStackTrace();
}
}
}
会被转化为:
public class Demo {
public Demo() {
}
public static void main(String[] args) {
try {
InputStream is = new FileInputStream("d:\\data.txt");
Throwable t = null;
try {
System.out.println(is);
} catch (Throwable e1) {
// t为代码出现的异常
t = e1;
throw e1;
} finally {
// 判断了资源不为空
if (is != null) {
// 如果代码有异常
if (t != null) {
try {
is.close();
} catch (Throwable e2) {
// 如果 close 出现异常,作为被压制异常添加
t.addSuppressed(e2); // 关闭资源释放时,期望将try块中的异常与关闭
//资源时的异常均不丢失都保留下来
}
} else {
// 如果代码没有异常,close 出现的异常就是最后 catch 块中的e
is.close();
}
}
}
}catch (IOException e){
e.printStackTrace();
}
}
}
? 为什么要设计一个addSuppressed(Throwable e) (添加被压制异常) 的方法呢?是为了防止异常信息的丢失(想想try-with. resources生成的fianlly 中如果抛出了异常) :
如以上代码所示,两个异常信息都不会丢失
⑩ 语法糖——匿名内部类
public class Demo8 {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("running...");
}
};
}
}
转换后的代码
public class Demo8 {
public static void main(String[] args) {
//用额外创建的类来创建匿名内部类对象
Runnable runnable = new Demo8$1();
}
}
//创建了一个额外的类,实现了Runnable接口
final class Demo8$1 implements Runnable {
public Demo8$1() {}
@Override
public void run() {
System.out.println("running...");
}
}
如果匿名内部类中引用了局部变量
public class Demo8 {
public static void main(String[] args) {
int x = 1;
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(x);
}
};
}
}
转化后代码
public class Demo8 {
public static void main(String[] args) {
int x = 1;
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(x);
}
};
}
}
final class Demo8$1 implements Runnable {
//多创建了一个变量
int val$x;
//变为了有参构造器
public Demo8$1(int x) {
this.val$x = x;
}
@Override
public void run() {
System.out.println(val$x);
}
}