为什么jdk代码样式使用变量赋值并在同一行读取 – 例如。 (i = 2)<max

我注意到在jdk源代码中,更具体地说,在集合框架中,在表达式中读取变量之前,首先要分配变量。 这只是一个简单的偏好还是更重要的我不知道的东西? 我能想到的一个原因是该变量仅在此表达式中使用。

由于我不习惯这种风格,我发现很难读懂它。 代码非常简洁。 您可以在下面看到从java.util.HashMap.getNode()获取的示例

 Node[] tab; Node first, e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 && ...) { ... } 

正如评论中已经提到的那样:Doug Lea是集合框架和并发软件包的主要作者之一,他们倾向于进行优化,这对于凡人来说可能看起来令人困惑(甚至违反直觉)。

这里的一个“着名”示例是将字段复制到局部变量 ,以便最小化字节码的大小,实际上也是在您引用的示例中使用table字段和本地tab变量完成的!


对于非常简单的测试,它似乎没有区别(指的是结果字节码大小)访问是否“内联”。 所以我尝试创建一个大致类似于你提到的getNode方法结构的例子:对数组的字段的访问,长度检查,对一个数组元素的字段的访问……

  • testSeparate方法确实分配了分配和检查
  • testInlined方法使用testInlined -in-if-style
  • testRepeated方法(作为反例)重复进行每次访问

代码:

 class Node { int k; int j; } public class AssignAndUseTestComplex { public static void main(String[] args) { AssignAndUseTestComplex t = new AssignAndUseTestComplex(); t.testSeparate(1); t.testInlined(1); t.testRepeated(1); } private Node table[] = new Node[] { new Node() }; int testSeparate(int value) { Node[] tab = table; if (tab != null) { int n = tab.length; if (n > 0) { Node first = tab[(n-1)]; if (first != null) { return first.k+first.j; } } } return 0; } int testInlined(int value) { Node[] tab; Node first, e; int n; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1)]) != null) { return first.k+first.j; } return 0; } int testRepeated(int value) { if (table != null) { if (table.length > 0) { if (table[(table.length-1)] != null) { return table[(table.length-1)].k+table[(table.length-1)].j; } } } return 0; } } 

结果字节码: testSeparate方法使用41条指令

  int testSeparate(int); Code: 0: aload_0 1: getfield #15 // Field table:[Lstackoverflow/Node; 4: astore_2 5: aload_2 6: ifnull 40 9: aload_2 10: arraylength 11: istore_3 12: iload_3 13: ifle 40 16: aload_2 17: iload_3 18: iconst_1 19: isub 20: aaload 21: astore 4 23: aload 4 25: ifnull 40 28: aload 4 30: getfield #37 // Field stackoverflow/Node.k:I 33: aload 4 35: getfield #41 // Field stackoverflow/Node.j:I 38: iadd 39: ireturn 40: iconst_0 41: ireturn 

testInlined方法确实有点小,有39条指令

  int testInlined(int); Code: 0: aload_0 1: getfield #15 // Field table:[Lstackoverflow/Node; 4: dup 5: astore_2 6: ifnull 38 9: aload_2 10: arraylength 11: dup 12: istore 5 14: ifle 38 17: aload_2 18: iload 5 20: iconst_1 21: isub 22: aaload 23: dup 24: astore_3 25: ifnull 38 28: aload_3 29: getfield #37 // Field stackoverflow/Node.k:I 32: aload_3 33: getfield #41 // Field stackoverflow/Node.j:I 36: iadd 37: ireturn 38: iconst_0 39: ireturn 

最后, testRepeated方法使用了高达63条指令

  int testRepeated(int); Code: 0: aload_0 1: getfield #15 // Field table:[Lstackoverflow/Node; 4: ifnull 62 7: aload_0 8: getfield #15 // Field table:[Lstackoverflow/Node; 11: arraylength 12: ifle 62 15: aload_0 16: getfield #15 // Field table:[Lstackoverflow/Node; 19: aload_0 20: getfield #15 // Field table:[Lstackoverflow/Node; 23: arraylength 24: iconst_1 25: isub 26: aaload 27: ifnull 62 30: aload_0 31: getfield #15 // Field table:[Lstackoverflow/Node; 34: aload_0 35: getfield #15 // Field table:[Lstackoverflow/Node; 38: arraylength 39: iconst_1 40: isub 41: aaload 42: getfield #37 // Field stackoverflow/Node.k:I 45: aload_0 46: getfield #15 // Field table:[Lstackoverflow/Node; 49: aload_0 50: getfield #15 // Field table:[Lstackoverflow/Node; 53: arraylength 54: iconst_1 55: isub 56: aaload 57: getfield #41 // Field stackoverflow/Node.j:I 60: iadd 61: ireturn 62: iconst_0 63: ireturn 

因此,似乎这种“模糊”的编写查询和赋值的方式实际上可以节省几个字节的字节码,并且(考虑到关于在局部变量中存储字段的链接答案中的理由),这可能是使用的原因。这种风格。

但…

在任何情况下:在方法执行几次之后,JIT将启动,并且生成的机器代码将与原始字节码“无关” – 我很确定所有三个版本实际上都是最后编译成相同的机器代码。

所以底线是:不要使用这种风格。 相反,只需编写易于阅读和维护的哑代码 。 你知道什么时候轮到你使用这样的“优化”了。


编辑:一个简短的附录……

我进行了进一步的测试,并比较了关于JIT生成的实际机器代码testSeparatetestInlined方法。

我稍微修改了main方法,以防止JIT可能采取的不切实际的过度优化或其他快捷方式,但实际方法未经修改。

正如所料:当使用热点反汇编JVM和-XX:+UnlockDiagnosticVMOptions -XX:+LogCompilation -XX:+PrintAssembly调用方法几千次时,两种方法的实际机器代码是相同的

因此,JIT再次完成其工作,程序员可以专注于编写可读代码(无论这意味着什么)。

……以及一个小的纠正/澄清:

我没有测试第三个方法testRepeated ,因为它不等同于其他方法(因此,它不能产生相同的机器代码)。 顺便说一下,这是在局部变量中存储字段的策略的另一个小优势:它提供了一种( 非常有限但有时很方便)的“ 线程安全 ”forms:它确保了数组的长度(如正在执行方法时, HashMapgetNode方法中的tab数组不能更改。