Java final 字段编译时常量表达式

Posted

技术标签:

【中文标题】Java final 字段编译时常量表达式【英文标题】:Java final field compile-time constant expression 【发布时间】:2013-07-04 14:04:46 【问题描述】:

以下文字来自jlshttp://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.5.3

即便如此,也存在许多并发症。如果最终字段是 初始化为编译时常量表达式(§15.28) 字段声明,可能不会观察到对最终字段的更改, 因为该 final 字段的使用在编译时被替换为 常量表达式的值。

谁能给我更好的解释以上。我无法理解语句“可能不会观察到对最终字段的更改”。可能在示例的帮助下。

【问题讨论】:

【参考方案1】:

我无法理解对 final 字段的语句更改可能不会被观察到

它告诉我们,如果最终变量被声明为编译时常量,那么在程序中进一步使用 reflection API 对最终变量所做的任何更改都不会在执行期间对程序可见。 例如考虑下面给出的代码:

import java.lang.reflect.*;
class ChangeFinal 

    private final int x = 20;//compile time constant
    public static void change(ChangeFinal cf)
    
        try
        
            Class clazz = ChangeFinal.class;
            Field field = clazz.getDeclaredField("x");
            field.setAccessible(true);
            field.set(cf , 190);//changed x to 190 for object cf
        
        catch (Exception ex)
        
            ex.printStackTrace();
        
    
    public static void main(String[] args) 
    
        ChangeFinal cf = new ChangeFinal();
        System.out.println(cf.x);//prints 20
        change(cf);
        System.out.println(cf.x);//prints 20
    

以上代码的输出为:

20
20

为什么? 答案在于javap -c 命令为 public static void main 提供的输出:

public static void main(java.lang.String[]);
  Code:
   0:   new     #3; //class ChangeFinal
   3:   dup
   4:   invokespecial   #11; //Method "<init>":()V
   7:   astore_1
   8:   getstatic       #12; //Field java/lang/System.out:Ljava/io/PrintStream;
   11:  aload_1
   12:  invokevirtual   #13; //Method java/lang/Object.getClass:()Ljava/lang/Cla
ss;
   15:  pop
   16:  bipush  20
   18:  invokevirtual   #14; //Method java/io/PrintStream.println:(I)V
   21:  aload_1
   22:  invokestatic    #15; //Method change:(LChangeFinal;)V
   25:  getstatic       #12; //Field java/lang/System.out:Ljava/io/PrintStream;
   28:  aload_1
   29:  invokevirtual   #13; //Method java/lang/Object.getClass:()Ljava/lang/Cla
ss;
   32:  pop
   33:  bipush  20
   35:  invokevirtual   #14; //Method java/io/PrintStream.println:(I)V
   38:  return


在第 16 行(在调用 changeFinal 方法之前)cf.x 的值被硬编码为 20 。在第 33 行(在调用 changeFinal 方法之后)cf.x 的值再次被硬编码为 20。因此,虽然最终变量x的值的变化是由reflection API在执行过程中成功完成的,但是由于x是一个编译时常量,它显示了它的常量值20

【讨论】:

【参考方案2】:

这意味着如果在课堂上你有这个:

public class Foo 
    public final boolean fooBoolean = true; // true is a constant expression
    public final int fooInt = 5; // 5 is a constant expression

在编译时,任何对Foo.fooBoolean 的引用都可以替换为true,对Foo.fooInt 的引用可以替换为5。如果在运行时您稍后通过反射更改了这些最终字段中的任何一个,那么引用它的代码(正如它所写的那样)可能永远看不到它。

【讨论】:

另一个例子是如果你在上面的类中将 fooInt 更改为 6,然后重新编译这个 Foo 类,而不重新编译使用 foo 类的类。那么这些其他类可能仍会将 fooInt 读取为 5 而不是 6。 @MTilsted 好例子。真的很混乱。【参考方案3】:

Java 程序很可能在不同时间观察到具有两个不同值的final 字段,即使没有反射,没有重新编译类的多个版本,也没有任何类似的东西。考虑下面的类:

class X 
    static final int x = getX();
    
    static int getX() 
        System.out.println("X.x is now " + X.x);
        return 1;
    
    
    public static void main(String[] args) 
        System.out.println("X.x is now " + X.x);
    

输出:

X.x is now 0
X.x is now 1

出现这种情况是因为部分代码(第一个println)在字段赋值之前执行,所以代码观察到字段的默认初始值0。字段在赋值之前有一个默认初始值, 即使它是最终的,因为它不是一个常量字段。您从 JLS 中引用的文本说,如果将字段声明为常量,则不会发生这种情况。

【讨论】:

以上是关于Java final 字段编译时常量表达式的主要内容,如果未能解决你的问题,请参考以下文章

Jave运行时常量池是啥意思?

Java 运行时常量池

Java——final关键字

C#TS和Dart对比3:编译时常量和运行时常量

重新精读《Java 编程思想》系列之final关键字

为啥 sizeof 表达式不是像 2、4、8 等这样的编译时常量?