问题 JLS是否需要内联最终的String常量?


我在操作某些字节码时遇到了一个问题 final  String 常量没有被java编译器(Java 8)内联,请参阅下面的示例:

public class MyTest
{
  private static final String ENABLED  = "Y";
  private static final String DISABLED = "N";

  private static boolean isEnabled(String key) {
      return key.equals("A");
  }

  private static String getString(String key, String value) {
      return key + value;
  }

  public static void main(String[] args) throws Exception {
    String flag = getString("F", isEnabled("A") ? ENABLED : DISABLED);
    System.out.println(flag);

    String flag2 = getString("F", isEnabled("A") ? ENABLED : DISABLED);
    System.out.println(flag2);
  }
}

使用javac生成的字节码(1.8.0_101)

public static void main(java.lang.String[]) throws java.lang.Exception;
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: ldc           #8                  // String F
         2: ldc           #2                  // String A
         4: invokestatic  #9                  // Method isEnabled:(Ljava/lang/String;)Z
         7: ifeq          16
        10: getstatic     #10                 // Field ENABLED:Ljava/lang/String;
        13: goto          19
        16: getstatic     #11                 // Field DISABLED:Ljava/lang/String;
        19: invokestatic  #12                 // Method getString:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
        22: astore_1
        23: getstatic     #13                 // Field java/lang/System.out:Ljava/io/PrintStream;
        26: aload_1
        27: invokevirtual #14                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        30: ldc           #8                  // String F
        32: ldc           #2                  // String A
        34: invokestatic  #9                  // Method isEnabled:(Ljava/lang/String;)Z
        37: ifeq          46
        40: getstatic     #10                 // Field ENABLED:Ljava/lang/String;
        43: goto          49
        46: getstatic     #11                 // Field DISABLED:Ljava/lang/String;
        49: invokestatic  #12                 // Method getString:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
        52: astore_2
        53: getstatic     #13                 // Field java/lang/System.out:Ljava/io/PrintStream;
        56: aload_2
        57: invokevirtual #14                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        60: return

你可以看到第二次出现这些字段 ENABLED 和 DISABLED 正在访问,编译器没有内联他们的值(使用 ldc),但改为使用 getstatic 直接访问该字段。使用其他编译器(Java 7,Eclipse)测试它并没有触发相同的行为,并且常量内联。

这可以被视为编译器错误,还是允许的  根据JLS,内联字符串常量一直存在?


6221
2017-09-08 08:24


起源

定义'内联'。 JLS要求 池。这就是你在说什么? - user207421
内联意味着用直接将常量值推入堆栈的指令替换getstatic指令,例如,在字符串的情况下为ldc,或者在整数基元的情况下为iconst_X / bipush / sipush / ...,... - T. Neidhart
@ piet.t:我删除了字节代码标记,因为语言规范确实没有处理字节代码。但是,那 行为 定义没有含糊不清,这允许回答问题。 - Holger
发现得好。这确实是一个错误,它已经修复了 JDK-8066871 它集成在JDK 9中并向后移植到JDK 1.8.0_102 - 这是1.8.0_101加上一个补丁集。请注意,JDK-8066871具有不同的症状,但相同的修复程序会更正该错误以及此处描述的错误。 - Stuart Marks
我刚刚提交了一个错误报告(JI-9043578),因为我在bug数据库中找不到相关条目。我猜它可以关闭。 - T. Neidhart


答案:


是的,规范规定了“内联”行为:

13.1。二进制形式

...

  1. 必须在编译时解析对作为常量变量(§4.12.4)的字段的引用 V 用常量变量的初始值表示。

    如果这样的领域是 static,然后在二进制文件的代码中不应该存在对该字段的引用,包括声明该字段的类或接口。这样的场必须总是看似已经初始化(§12.4.2);字段的默认初始值(如果不同于 V)绝不能被观察到。

    如果这样的字段是非static,然后在二进制文件的代码中不应该引用该字段,除了包含该字段的类。 (它将是一个类而不是一个接口,因为接口只有 static 字段。)类应该有代码来设置字段的值 V 在实例创建期间(第12.5节)。

请注意,这是如何精确地解决您的情况:“如果这样的字段是 static,那么在二进制文件的代码中不应该引用该字段, 包括声明该字段的类或接口”。

换句话说,如果遇到编译器不遵守此问题,则会发现编译器错误。


作为附录,查找此信息的起点是:

4.12.4。最终变量

...

一个 常数变量 是一个 final 原始类型或类型的变量 String 用常量表达式初始化(§15.28)。变量是否是常量变量可能会对类初始化产生影响(§12.4.1),二进制兼容性(§13.1§13.4.9)和明确的任务(§16(确定任务))。


10
2017-09-08 10:54



非常感谢我正在寻找的东西,所以这证实了观察到的行为确实是编译器错误。 - T. Neidhart
我检查了除二进制兼容性之外的所有引用:) - Marko Topolnik