语法糖(Syntactic Sugar)的出现是为了降低我们编写某些代码时陷入的重复或繁琐,这使得我们使用语法糖后可以写出简明而优雅的代码。在Java中不加工的语法糖代码运行时可不会被虚拟机接受,因此编译器为了让这些含有语法糖的代码正常工作其实需要对这些代码进行加工,经过编译器在生成class字节码的阶段完成解语法糖(desugar)的过程,那么这些语法糖最终究竟被编译成了什么呢,在这里列举了如下的一些Java典型的语法糖,结合实例和它们的编译结果分析一下。本文为本系列第二篇。
枚举类
枚举在编译后会变成一个特殊的final类,因此枚举类型是名副其实的不可变类,我们通过下面最简单的例子来仔细分析一下:
源码:
enum COLOR {
RED,
BLUE,
GREEN
}
使用这个枚举的时候我们可以发现有valueOf(String)
和values()
这样的方法可以用,因此不难猜测编译器会添加一些未在源码中出现的其他增强二进制字节码,可以看一下具体的字节码:
final class COLOR extends java.lang.Enum<COLOR>
minor version: 0
major version: 52
flags: ACC_FINAL, ACC_SUPER, ACC_ENUM
...
{
public static final COLOR RED;
descriptor: LCOLOR;
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM
public static final COLOR BLUE;
descriptor: LCOLOR;
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM
public static final COLOR GREEN;
descriptor: LCOLOR;
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM
public static COLOR[] values();
descriptor: ()[LCOLOR;
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: getstatic #1 // Field $VALUES:[LCOLOR;
3: invokevirtual #2 // Method "[LCOLOR;".clone:()Ljava/lang/Object;
6: checkcast #3 // class "[LCOLOR;"
9: areturn
public static COLOR valueOf(java.lang.String);
descriptor: (Ljava/lang/String;)LCOLOR;
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: ldc #4 // class COLOR
2: aload_0
3: invokestatic #5 // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
6: checkcast #4 // class COLOR
9: areturn
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=4, locals=0, args_size=0
0: new #4 // class COLOR
3: dup
4: ldc #7 // String RED
6: iconst_0
7: invokespecial #8 // Method "<init>":(Ljava/lang/String;I)V
10: putstatic #9 // Field RED:LCOLOR;
13: new #4 // class COLOR
16: dup
17: ldc #10 // String BLUE
19: iconst_1
20: invokespecial #8 // Method "<init>":(Ljava/lang/String;I)V
23: putstatic #11 // Field BLUE:LCOLOR;
26: new #4 // class COLOR
29: dup
30: ldc #12 // String GREEN
32: iconst_2
33: invokespecial #8 // Method "<init>":(Ljava/lang/String;I)V
36: putstatic #13 // Field GREEN:LCOLOR;
39: iconst_3
40: anewarray #4 // class COLOR
43: dup
44: iconst_0
45: getstatic #9 // Field RED:LCOLOR;
48: aastore
49: dup
50: iconst_1
51: getstatic #11 // Field BLUE:LCOLOR;
54: aastore
55: dup
56: iconst_2
57: getstatic #13 // Field GREEN:LCOLOR;
60: aastore
61: putstatic #1 // Field $VALUES:[LCOLOR;
64: return
}
Signature: #32 // Ljava/lang/Enum<LCOLOR;>;
这段字节码可以证实出上面的猜测,确实会有额外的二进制字节码被添加了,枚举类会被编译成为Ljava/lang/Enum
的子类COLOR
,而枚举类型中的枚举项会被编译成为COLOR
类的常量字段,而且COLOR
内部还会维护一个数组来保存这些常量字段,并进而添加valueOf(String)
和values()
来访问这个数组。因此,对应地我们可以翻译这段二进制字节码为这样的代码:
final class COLOR extends Enum<COLOR> {
private static final COLOR RED;
private static final COLOR BLUE;
private static final COLOR GREEN;
private static final COLOR[] $VALUES;
static {
RED = new COLOR("RED", 0);
BLUE = new COLOR("BLUE", 1);
GREEN = new COLOR("GREEN", 2);
COLOR[] $COLOR_ARRAY = new COLOR[3];
$COLOR_ARRAY[0] = RED;
$COLOR_ARRAY[1] = BLUE;
$COLOR_ARRAY[2] = GREEN;
$VALUES = $COLOR_ARRAY;
}
private COLOR(String color, int ordinal) {
super(color, ordinal);
}
public static COLOR[] values() {
return $VALUES.clone();
}
public static COLOR valueOf(String color) {
return Enum.valueOf(COLOR.class, color);
}
}
注意,这段代码并不能通过编译,因为源码这一层是不允许直接继承Ljava/lang/Enum
的,这个继承过程只允许在编译器内部解语法糖的过程中被编译器添加,添加之后的类才会有ACC_ENUM
的访问标识符。
我们可以看到的是在Ljava/lang/Enum
内部实际上有name
和ordinal
常量来标识一个枚举项,name
会由枚举项名来设置,而ordinal
是枚举项序号,由枚举项排列顺序决定。
我们再来看一下带有字段的枚举项编译后的效果。
源码:
enum COLOR {
RED(0),
BLUE(1),
GREEN(2);
int code;
COLOR(int code) {
this.code = code;
}
}
编译后的字节码:
final class COLOR extends java.lang.Enum<COLOR>
minor version: 0
major version: 52
flags: ACC_FINAL, ACC_SUPER, ACC_ENUM
...
{
public static final COLOR RED;
descriptor: LCOLOR;
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM
public static final COLOR BLUE;
descriptor: LCOLOR;
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM
public static final COLOR GREEN;
descriptor: LCOLOR;
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM
int code;
descriptor: I
flags:
public static COLOR[] values();
descriptor: ()[LCOLOR;
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: getstatic #1 // Field $VALUES:[LCOLOR;
3: invokevirtual #2 // Method "[LCOLOR;".clone:()Ljava/lang/Object;
6: checkcast #3 // class "[LCOLOR;"
9: areturn
LineNumberTable:
line 1: 0
public static COLOR valueOf(java.lang.String);
descriptor: (Ljava/lang/String;)LCOLOR;
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: ldc #4 // class COLOR
2: aload_0
3: invokestatic #5 // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
6: checkcast #4 // class COLOR
9: areturn
LineNumberTable:
line 1: 0
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=5, locals=0, args_size=0
0: new #4 // class COLOR
3: dup
4: ldc #8 // String RED
6: iconst_0
7: iconst_0
8: invokespecial #9 // Method "<init>":(Ljava/lang/String;II)V
11: putstatic #10 // Field RED:LCOLOR;
14: new #4 // class COLOR
17: dup
18: ldc #11 // String BLUE
20: iconst_1
21: iconst_1
22: invokespecial #9 // Method "<init>":(Ljava/lang/String;II)V
25: putstatic #12 // Field BLUE:LCOLOR;
28: new #4 // class COLOR
31: dup
32: ldc #13 // String GREEN
34: iconst_2
35: iconst_2
36: invokespecial #9 // Method "<init>":(Ljava/lang/String;II)V
39: putstatic #14 // Field GREEN:LCOLOR;
42: iconst_3
43: anewarray #4 // class COLOR
46: dup
47: iconst_0
48: getstatic #10 // Field RED:LCOLOR;
51: aastore
52: dup
53: iconst_1
54: getstatic #12 // Field BLUE:LCOLOR;
57: aastore
58: dup
59: iconst_2
60: getstatic #14 // Field GREEN:LCOLOR;
63: aastore
64: putstatic #1 // Field $VALUES:[LCOLOR;
67: return
LineNumberTable:
line 2: 0
line 3: 14
line 4: 28
line 1: 42
}
Signature: #36 // Ljava/lang/Enum<LCOLOR;>;
用java源码翻译下上面的结果:
final class COLOR extends Enum<COLOR> {
private static final COLOR RED;
private static final COLOR BLUE;
private static final COLOR GREEN;
int code;
private static final COLOR[] $VALUES;
static {
RED = new COLOR("RED", 0, 0);
BLUE = new COLOR("BLUE", 1, 1);
GREEN = new COLOR("GREEN", 2, 2);
COLOR[] $COLOR_ARRAY = new COLOR[3];
$COLOR_ARRAY[0] = RED;
$COLOR_ARRAY[1] = BLUE;
$COLOR_ARRAY[2] = GREEN;
$VALUES = $COLOR_ARRAY;
}
private COLOR(String color, int ordinal, int code) {
super(color, ordinal);
this.code = code;
}
public static COLOR[] values() {
return $VALUES.clone();
}
public static COLOR valueOf(String color) {
return Enum.valueOf(COLOR.class, color);
}
}
其实有了之前的基础很容易看出来,新增加的code
字段最终只是变成了编译器生成的COLOR
类的一个字段,唯一的变化就是编译出的初始化方法也会增加为这个字段而添加的参数。
断言
java 1.4引入的断言,使用关键字assert
来判断一个条件是否为true,通过如下的源码来分析一下:
class Main {
public static void main(String[] args) {
String judge = "yes";
assert "no".equals(judge);
}
}
断言在运行时默认是关闭的,我们可以通过运行时打开断言来启用:java -ea Main
:
at Main.main(Main.java:4)
那么我们来看一下编译后的字节码:
{
static final boolean $assertionsDisabled;
descriptor: Z
flags: ACC_STATIC, ACC_FINAL, ACC_SYNTHETIC
Main();
descriptor: ()V
flags:
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: ldc #2 // String yes
2: astore_1
3: getstatic #3 // Field $assertionsDisabled:Z
6: ifne 26
9: ldc #4 // String no
11: aload_1
12: invokevirtual #5 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
15: ifne 26
18: new #6 // class java/lang/AssertionError
21: dup
22: invokespecial #7 // Method java/lang/AssertionError."<init>":()V
25: athrow
26: return
StackMapTable: number_of_entries = 1
frame_type = 252 /* append */
offset_delta = 26
locals = [ class java/lang/String ]
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: ldc #8 // class Main
2: invokevirtual #9 // Method java/lang/Class.desiredAssertionStatus:()Z
5: ifne 12
8: iconst_1
9: goto 13
12: iconst_0
13: putstatic #3 // Field $assertionsDisabled:Z
16: return
StackMapTable: number_of_entries = 2
frame_type = 12 /* same */
frame_type = 64 /* same_locals_1_stack_item */
stack = [ int ]
}
可以发现编译器为Main
类添加了字段$assertionsDisabled
,此字段即是启用断言的关键。在运行时加入启用断言的-ea
会使得类初始化时Class.desiredAssertionStatus
为真,进而字段$assertionsDisabled
为真,这个逻辑在上述的字节码中可以看出。在断言的地方,如果条件为真则会正常返回,如果条件为false
则会抛出java/lang/AssertionError
错误导致程序终止。
用java源码翻译下上面的结果:
class Main {
private static final boolean $assertionsDisabled;
static {
if (Main.class.desiredAssertionStatus()) {
$assertionsDisabled = true;
} else {
$assertionsDisabled = false;
}
}
public static void main(String[] args) {
if($assertionsDisabled) {
if (!"no".equals("yes")) {
throw new AssertionError();
}
}
}
}
switch处理枚举和字符串
我们先来看看在java 1.7以前就可以使用switch的类型在字节码层是如何工作的,这里以int
类型为例:
class Main {
public static void main(String[] args) {
int a = 1;
switch (a) {
case 0:
System.out.println("0");
break;
case 2:
System.out.println("1");
break;
case 8:
System.out.println("3");
break;
default:
break;
}
}
}
编译后的字节码:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: iconst_1
1: istore_1
2: iload_1
3: lookupswitch { // 3
0: 36
2: 47
8: 58
default: 69
}
36: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
39: ldc #3 // String 0
41: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
44: goto 69
47: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
50: ldc #5 // String 1
52: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
55: goto 69
58: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
61: ldc #6 // String 3
63: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
66: goto 69
69: return
}
这里是用的字节码命令lookupswitch
适用于判断switch的跳转语句的,即如果switch 0
跳转到26行、switch 2
跳转到47行、switch 8
跳转到58行、其他跳转到69行。
受限于lookupswitch
判断的条件的类型,在java 1.7以前是无法对非32位数字类型的类型做判断的,而java 1.7以后通过语法糖的解析实现了字符串的switch分支判断,可以想到的是,在不改变lookupswitch
的能力的情况下,编译器会将字符串转换为32位数字。我们写这样的例子来分析下:
class Main {
public static void main(String[] args) {
String a = args[0];
switch (a) {
case "a":
System.out.println("a");
break;
case "b":
System.out.println("b");
break;
default:
break;
}
}
}
编译后的结果:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: aload_0
1: iconst_0
2: aaload
3: astore_1
4: aload_1
5: astore_2
6: iconst_m1
7: istore_3
8: aload_2
9: invokevirtual #2 // Method java/lang/String.hashCode:()I
12: lookupswitch { // 2
97: 40
98: 54
default: 65
}
40: aload_2
41: ldc #3 // String a
43: invokevirtual #4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
46: ifeq 65
49: iconst_0
50: istore_3
51: goto 65
54: aload_2
55: ldc #5 // String b
57: invokevirtual #4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
60: ifeq 65
63: iconst_1
64: istore_3
65: iload_3
66: lookupswitch { // 2
0: 92
1: 103
default: 114
}
92: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
95: ldc #3 // String a
97: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
100: goto 114
103: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
106: ldc #5 // String b
108: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
111: goto 114
114: return
LocalVariableTable:
Start Length Slot Name Signature
0 115 0 args [Ljava/lang/String;
4 111 1 a Ljava/lang/String;
}
我们可以发现编译器对要做分支判断的字符串计算了它的hashcode,而这个hashcode是符合lookupswitch
要求的32位数字,因此将这个hashcode做lookupswitch
分支判断,和switch条件中的"a"
、"b"
的hashcode做比较,如果进入了其中某个分支如"a"
分支,则在分支中判断"a"
和字符串是否相等,如果相等则确定此分支是正确的(只有hashcode相等并不能确定是值相等,hashcode的冲突原理不再展开),接下来再将分支条件直接设置为0、1、2这样的简单条件执行下一轮lookupswitch
。我们同样可以用如下java源码翻译下上面的结果:
class Main {
public static void main(String[] args) {
String param = args[0];
int hashcode = param.hashCode();
final int condition_a = 97; //"a".hashCode()
final int condition_b = 98; //"b".hashCode();
int hashcodeSwitchResult = -1;
switch (hashcode) {
case condition_a:
if("a".equals(param)){
hashcodeSwitchResult = 0;
}
break;
case condition_b:
if("b".equals(param)){
hashcodeSwitchResult = 1;
}
break;
default:
break;
}
switch (hashcodeSwitchResult) {
case 0:
System.out.println("a");
break;
case 1:
System.out.println("b");
break;
default:
break;
}
}
}