本文介绍的两个语法糖均与条件相关,由于它们并不太常见,所以只会用很短的篇幅介绍。
条件编译
一般情况下,每一行代码都要参与编译。但有时,出于某种原因,可能只希望对代码的一部分进行编译。这时,需要在程序中加上条件,编译器可以只对满足条件的代码进行编译,并丢弃不满足条件的部分,这就是条件编译。
诸如 C、C++ 等许多语言提供了预处理的功能,并通过预处理来实现条件编译。例如下面这段 C 代码在 DEBUG 模式编译代码块1,其他情况编译代码块2:
#ifdef DEBUG
code block 1
#else
code block 2
#endif
复制代码
Java 并没有提供类似的预处理功能,但是 Java 也可以实现条件编译。考虑如下代码:
private static final boolean DEBUG = true;
public static void main(String[] args) {
if (DEBUG) {
System.out.println("block 1");
} else {
System.out.println("block 2");
}
}
复制代码
反编译字节码文件得到 main 方法的实际内容:
public static void main(java.lang.String[]);
Code:
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #4 // String block 1
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
复制代码
换句话说,编译后的 main 方法与下面的代码等价:
public static void main(String[] args) {
System.out.println("block 1");
}
复制代码
可以看到,编译器会做优化,对于条件必然为 false 的语句块,编译器不会为其生成字节码。
由以上可见,Java 的条件编译是借助条件为常量的条件语句来实现的,因此,Java 的条件编译只能局限在语句块级别。相比之下,C、C++ 等语言由于有预处理,因此可以实现文件级别的条件编译。
断言语句
Java 1.4 引入了断言,用以指明程序到达某个执行点时所必然满足的条件。
考虑如下求倒数的代码,我们使用断言来指明所指定的数字不能为 0:
public class Test {
public static void main(String[] args) {
System.out.println(inverse(2.0));
}
private static double inverse(double d) {
assert d != 0;
return 1.0 / d;
}
}
复制代码
反编译,得到(此处省去了构造方法和main方法):
public class com.aliyun.yqtest.Test {
static final boolean $assertionsDisabled;
private static double inverse(double);
Code:
0: getstatic #7 // Field $assertionsDisabled:Z
3: ifne 20
6: dload_0
7: dconst_0
8: dcmpl
9: ifne 20
12: new #8 // class java/lang/AssertionError
15: dup
16: invokespecial #9 // Method java/lang/AssertionError."<init>":()V
19: athrow
20: dconst_1
21: dload_0
22: ddiv
23: dreturn
static {};
Code:
0: ldc #10 // class com/aliyun/yqtest/Test
2: invokevirtual #11 // Method java/lang/Class.desiredAssertionStatus:()Z
5: ifne 12
8: iconst_1
9: goto 13
12: iconst_0
13: putstatic #7 // Field $assertionsDisabled:Z
16: return
}
复制代码
可以看到,编译过后,方法所处的类中多了一个 static final boolean 字段 $assertionsDisabled
,并且还被添加了一段静态代码块用于初始化它。事实上,这个字段就是断言是否开启的标识。在 inverse 方法内部,先根据 $assertionsDisabled
字段检查断言是否开启。如果未开启,则直接跳转到指令 20,继续执行求倒数的逻辑;如果开启,则检查条件是否满足,如果条件满足则跳至指令 20,否则创建一个 AssertionError 并抛出。简单地讲,编译后的 inverse 方法可以看做下面这样:
private static double inverse(double d) {
if (!$assertionsDisabled && d == 0) {
throw new AssertionError();
}
return 1.0 / d;
}
复制代码
总结一下,断言在编译过后被转化成了语句所处类中的一个 static final boolean 字段,并在类初始化阶段借助静态语句块完成其初始化。在程序执行过程中,直接根据该字段判断断言是否开启,来决定是否执行断言检查。