Javac缺少有效final的优化

7

事实:

javac 被编程成可以检测一个变量是否是final,或者它是否可以被视为有效的final

证明:

这段代码展示了这一点。

public static void finalCheck() {
        String str1 = "hello";
        Runnable r = () -> {
             str1 = "hello";
        };
}

这段代码无法编译,因为编译器能够检测到函数中重新分配了 String 引用 str1

现在有一个情况:

情况1:

Javac 对于 finalString 实例进行了优化,避免创建 StringBuilder 和相关操作。

证明:

以下是这个 Java 方法的代码

  public static void finalCheck() {
    final String str1 = "hello";
    final String str2 = "world";
    String str3 = str1 + " " + str2;
    System.out.println(str3);
  }

编译结果为

  public static void finalCheck();
    Code:
       0: ldc           #3                  // String hello world
       2: astore_2
       3: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
       6: aload_2
       7: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      10: return

问题:

但是现在当我们将它们有效地作为 final

public static void finalCheck() {
    String str1 = "hello";
    String str2 = "world";
    String str3 = str1 + " " + str2;
    System.out.println(str3);
}

它不会以相似的方式进行优化,最终会编译成。
  public static void finalCheck();
    Code:
       0: ldc           #3                  // String hello
       2: astore_0
       3: ldc           #4                  // String world
       5: astore_1
       6: aload_0
       7: aload_1
       8: invokedynamic #5,  0              // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
      13: astore_2
      14: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
      17: aload_2
      18: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      21: return

JVM

$java -version
java version "10" 2018-03-20
Java(TM) SE Runtime Environment 18.3 (build 10+46)
Java HotSpot(TM) 64-Bit Server VM 18.3 (build 10+46, mixed mode)

编译器
$javac -version
javac 10

问题:为什么它不优化有效最终结果?


5
你有什么问题? - shmosel
@shmosel明确指出了问题,感谢您指出。 - jmj
2
没有必要进行这种优化,实际上对于第一个“final”情况也是如此。为什么会有这样的差异,除非你能请到编译器的作者,否则任何回答都主要基于个人意见。 - user207421
javac 不支持这样做是因为它被规定为不支持。然而,我认为jit编译器能够处理这个问题,从而使得两个示例在效率上相等。然而,要证明这一点,我们需要一个微基准测试。 - CoronA
1
@CoronA 这并没有“被指定为这样”。它也没有被指定为任何一种方式。如果您认为有所不同,请提供来自JLS的引用。 - user207421
1
@EJP,令人惊讶的是,规范足够明确,可以强制要求javac展示这两种情况的行为。我添加了答案... - Holger
1个回答

6

引入“有效终态”概念并未影响常量表达式和字符串拼接的规则。

请参阅Java® 语言规范,§15.18.1 字符串拼接运算符 +

如果表达式不是常量表达式(§15.28),则会新创建一个 String 对象(§12.5)。

在引用的章节§12.5 创建新类实例中,消除了任何疑虑:

执行非常量表达式(§15.28)的字符串拼接运算符 +§15.18.1)总是创建一个新的 String 对象来表示结果。

因此,尽管某些结构可能具有可预测的字符串结果,即使不是常量表达式,用常量结果替换它们将违反规范。只有常量表达式可以(甚至必须)在编译时被其常量值替换。关于引用变量,§15.28 指出它们必须是根据 §4.12.4 的常量变量才能成为常量表达式:

“常量变量”是指使用常量表达式初始化的原始类型或类型 Stringfinal 变量(§15.28)。

请注意常量变量必须是 final 类型。

还有隐含的终态变量概念,这与“有效终态”是不同的:

三种类型的变量会隐式声明为 final:接口字段(§9.3),在 try-with-resources 语句中声明为资源的局部变量(§14.20.3)以及多个 catch 子句的异常参数(§14.20)。单个 catch 子句的异常参数从不被隐式声明为 final,但可能是 effectively final。

因此,很明显,接口字段是隐式声明为 final 的(它们也是隐式声明为 static 的),因为它们一直都是这样,而其他两种情况下隐式声明为 final 的变量永远不会是字符串或基元类型,因此永远不是常量。

Effectively final 变量在某些用例中与 final 变量一样特殊(例如,在 Java 7 中改进类型检查时重新抛出已捕获的异常)(自 Java 8 以来,它们可以被 lambda 表达式和内部类引用(捕获),自 Java 9 以来,可以使用 try-with-resources 引用它们 (try(existingVariable) {…}),但除此之外,它们不像 final 变量那样对待。


1
没错,但是有优化的机会,我正在寻找编译器跳过它的技术原因。如果这样做,是否会有任何技术上的问题? - jmj
2
你的意思是,违反规范不足以成为不这样做的理由吗? - Holger
2
顺便说一下,你说你用了javac 10,但是字节码看起来像Java 9之前的版本,这让我很烦恼。你使用了-target--release选项吗? - Holger
1
@Holger 嗯,是的,在当前形式下违反了它,但为什么不改变呢?我的意思是这里显然有改进的空间,这种情况下字节码要小得多。他们引入了有效的final,为什么不进一步支持呢?我不明白。虽然我理解规范的推理,但其他方面我有点困惑。不过你的回答很好。 - Eugene
3
正如您在我的答案中所看到的,“effectively final”是在Java 7中为了一个非常有限的特性而引入的,并逐渐随着每个版本得到新的用例。因此,如果它在未来的版本中增加了更多功能,例如JEP303建议如果使用常量表达式初始化,那么“constant expressions”可能包括有效的final变量。由于它与JEP 309相关,该版本针对Java 11,因此它可能不会持续太久。但对于“为什么我们今天没有它?”的答案是“因为它在过去的20年中没有改变”... - Holger
1
@Holger - 你关于字节码版本的说法是正确的,这是 IDE 配置错误导致的。我使用外部 JDK 工具添加了原始字节码。 - jmj

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接