Java 编译器是否优化了不必要的三元运算符?
我一直在审查代码,其中一些编码人员一直在使用冗余的三元运算符“以提高可读性”。如:
boolean val = (foo == bar && foo1 != bar) ? true : false;
显然,最好只是将语句的结果分配给变量,但是编译器关心吗?boolean
我一直在审查代码,其中一些编码人员一直在使用冗余的三元运算符“以提高可读性”。如:
boolean val = (foo == bar && foo1 != bar) ? true : false;
显然,最好只是将语句的结果分配给变量,但是编译器关心吗?boolean
我发现不必要的三元运算符的使用往往会使代码更加混乱,可读性更差,这与初衷相反。
话虽如此,编译器在这方面的行为可以通过比较JVM编译的字节码来轻松测试。
下面是两个模拟类来说明这一点:
情况 I(不含三元运算符):
class Class {
public static void foo(int a, int b, int c) {
boolean val = (a == c && b != c);
System.out.println(val);
}
public static void main(String[] args) {
foo(1,2,3);
}
}
情况 II(使用三元运算符):
class Class {
public static void foo(int a, int b, int c) {
boolean val = (a == c && b != c) ? true : false;
System.out.println(val);
}
public static void main(String[] args) {
foo(1,2,3);
}
}
案例 I 中 foo() 方法的字节码:
0: iload_0
1: iload_2
2: if_icmpne 14
5: iload_1
6: iload_2
7: if_icmpeq 14
10: iconst_1
11: goto 15
14: iconst_0
15: istore_3
16: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
19: iload_3
20: invokevirtual #3 // Method java/io/PrintStream.println:(Z)V
23: return
案例 II 中 foo() 方法的字节码:
0: iload_0
1: iload_2
2: if_icmpne 14
5: iload_1
6: iload_2
7: if_icmpeq 14
10: iconst_1
11: goto 15
14: iconst_0
15: istore_3
16: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
19: iload_3
20: invokevirtual #3 // Method java/io/PrintStream.println:(Z)V
23: return
请注意,在这两种情况下,字节码是相同的,即编译器在编译布尔值时忽略三元运算符。val
编辑:
关于这个问题的对话已经朝着几个方向之一发展。
如上所示,在这两种情况下(无论是否使用冗余三元),编译后的java字节码都是相同的。
这是否可视为 Java 编译器的优化,在某种程度上取决于您对优化的定义。在某些方面,正如在其他答案中多次指出的那样,争论“不”是有道理的 - 它不是一种优化,而是在这两种情况下,生成的字节码都是执行此任务的最简单的堆栈操作集,而不管三元。
但是关于主要问题:
显然,最好将语句的结果分配给布尔变量,但是编译器关心吗?
简单的答案是否定的。编译器不在乎。
与Pavel Horal,Codo和yuvgin的答案相反,我认为编译器不会优化(或忽略)三元运算符。(澄清:我指的是Java到Bytecode编译器,而不是JIT)
请参阅测试用例。
第 1 类:计算布尔表达式,将其存储在变量中,然后返回该变量。
public static boolean testCompiler(final int a, final int b)
{
final boolean c = ...;
return c;
}
因此,对于不同的布尔表达式,我们检查字节码:1. 表达式:a == b
字节码
0: iload_0
1: iload_1
2: if_icmpne 9
5: iconst_1
6: goto 10
9: iconst_0
10: istore_2
11: iload_2
12: ireturn
a == b ? true : false
字节码
0: iload_0
1: iload_1
2: if_icmpne 9
5: iconst_1
6: goto 10
9: iconst_0
10: istore_2
11: iload_2
12: ireturn
a == b ? false : true
字节码
0: iload_0
1: iload_1
2: if_icmpne 9
5: iconst_0
6: goto 10
9: iconst_1
10: istore_2
11: iload_2
12: ireturn
情况(1)和(2)编译为完全相同的字节码,不是因为编译器优化了三元运算符,而是因为它基本上需要每次都执行这个平凡的三元运算符。它需要在字节码级别指定是返回 true 还是 false。要验证这一点,请查看案例 (3)。它是完全相同的字节码,除了交换的第5行和第9行。
然后会发生什么,当反编译产生时?反编译器的选择是选择最简单的路径。a == b ? true : false
a == b
此外,基于“Class 1”实验,可以合理地假设在转换为字节码的方式上与 完全相同。然而,事实并非如此。为了测试我们检查以下“类2”,与“类1”的唯一区别是,它不将布尔结果存储在变量中,而是立即返回它。a == b ? true : false
a == b
第 2 类:计算布尔表达式并返回结果(不将其存储在变量中)
public static boolean testCompiler(final int a, final int b)
{
return ...;
}
a == b
字节码:
0: iload_0
1: iload_1
2: if_icmpne 7
5: iconst_1
6: ireturn
7: iconst_0
8: ireturn
a == b ? true : false
字节码
0: iload_0
1: iload_1
2: if_icmpne 9
5: iconst_1
6: goto 10
9: iconst_0
10: ireturn
a == b ? false : true
字节码
0: iload_0
1: iload_1
2: if_icmpne 9
5: iconst_0
6: goto 10
9: iconst_1
10: ireturn
这里很明显,和 表达式的编译方式不同,因为情况 (1) 和 (2) 产生不同的字节码(情况 (2) 和 (3),正如预期的那样,只有第 5,9 行被交换)。a == b
a == b ? true : false
起初,我发现这令人惊讶,因为我期望所有3个案例都是相同的(不包括案例(3)的交换行5,9)。当编译器遇到时,它会计算表达式并在遇到它使用 to to line 的地方后立即返回。我理解这样做是为了在三元运算符的“true”情况下为潜在语句留出空间:在检查和行之间。即使在这种情况下它只是一个布尔值,编译器也会像在一般情况下处理它一样处理它,因为存在更复杂的块。
另一方面,“Class 1”实验掩盖了这一事实,因为在分支中也有,并且不仅在情况(1)和(2)中强制命令并导致完全相同的字节码。a == b
a == b ? true : false
goto
ireturn
if_icmpne
goto
true
true
istore
iload
ireturn
goto
作为关于测试环境的注意事项,这些字节码是使用最新的Eclipse(4.10)生成的,Eclipse使用各自的ECJ编译器,与IntelliJ IDEA使用的javac不同。
但是,在其他答案(使用IntelliJ)中读取javac生成的字节码,我相信相同的逻辑也适用于那里,至少对于存储值并且不会立即返回的“Class 1”实验也是如此。
最后,正如在其他答案(例如supercat和jcsahnwaldt的答案)中已经指出的那样,在这个线程和SO的其他问题中,繁重的优化是由JIT编译器完成的,而不是从java-->java-bytecode编译器完成的,所以这些检查虽然对字节码转换很有帮助,但并不能很好地衡量最终优化代码将如何执行。
补充:jcsahnwaldt的答案比较了javac和ECJ在类似情况下产生的字节码
(作为免责声明,我没有对Java编译或反汇编进行过太多的研究,以真正了解它在引擎盖下的作用;我的结论主要基于上述实验的结果。