Java静态常量值在编译时替换?

19

在Java中,假设我有以下代码:

==fileA.java==
class A
{  
    public static final int SIZE = 100;
}  

然后在另一个文件中我使用这个值

==fileB.java==  
import A;
class b
{
      Object[] temp = new Object[A.SIZE];
}

当代码编译后,SIZE 是否会被替换为100?如果我只替换FileA.jar而不替换FileB.jar,对象数组是否会获得新值,还是因为它在最初构建时已经硬编码为100?


4
你的意思是 new Object[A.SIZE]; 吗? - Bala R
1
你应该在这里得到一个编译器错误。 - Sid
10个回答

32

是的,Java编译器确实会用字面值替换静态常量值,例如你的例子中的SIZE

因此,如果你稍后更改了类A中的SIZE但没有重新编译类b,你仍然会在类b中看到旧值。你可以很容易地测试一下:

文件A.java

public class A {
    public static final int VALUE = 200;
}

文件 B.java

public class B {
    public static void main(String[] args) {
        System.out.println(A.VALUE);
    }
}

编译 A.java 和 B.java。现在运行:java B

更改 A.java 中的值。重新编译 A.java,但不编译 B.java。再次运行,您会看到旧值被打印出来。


这是我认为它应该做的,但下面也有几个答案说相反的... 有没有文档支持其中任何一个方向? - Without Me It Just Aweso
3
有没有什么方法可以避免编译器这样做? - landry
@jesper:这个问题我见过的最好的答案之一。帮了我很多! - Giri

8
您可以通过以下方式避免常量被编译到B中:
class A
{  
    public static final int SIZE;

    static 
    {
        SIZE = 100;
    }
}  

1
谢谢:这对我在一个奇怪的情况下很有帮助:我正在使用ikvm将Java编译为.NET,但我的静态final字段在C#中被渲染为“const”(具有相同的内联行为)。我希望ikvm生成“static readonly”,它是常量,但在依赖程序集中不会被内联。我将您的解决方案应用于我的Java代码,并且ikvm优雅地生成了public static readonly in SIZE = 100; 在C#中可以正常工作! - odalet
嗨@MeBigFatGuy,你能解释一下我们在内联定义常量时(例如final static final String str =“some big string”)与使用静态块(static {str =“some big string”; })的区别吗?为什么静态常量会被编译成引用它们的类?谢谢。 - Lalit Rao
当您定义一个public static final String时,发生的所有事情都是无论您在哪里引用该字段,编译器都将注入一个LDC操作码,该操作码引用该类的常量池,在其中发出LDC。因此,如果您在一个类中定义一个字段,但在100个类中引用该变量,则这另外100个类中的每个类的常量池都会获得该String的副本,并且这些其他类不会引用原始类以获取String的值。如果您修改并编译原始类而不编译其他100个类,则可能会导致问题。 - MeBigFatGuy
如果所涉及的字符串非常大,那么你将会有一堆非常大的类文件。无论哪个类引用了该字段都会很大。如果你真的在处理大量的字符串,最好将它们放在类路径上的属性文件中。 - MeBigFatGuy

5

证明这种行为的另一种方法是查看生成的字节码。当常量“小”(可能 < 128)时:

public B();
  Code:
   0:   aload_0
   1:   invokespecial   #10; //Method java/lang/Object."<init>":()V
   4:   aload_0
   5:   bipush  42
   7:   anewarray       #3; //class java/lang/Object
   10:  putfield        #12; //Field temp:[Ljava/lang/Object;
   13:  return

}

(我用42代替100,以便更加突出。)在这种情况下,它明显地被替换为字节码。但是,假设常量“更大”,那么你会得到像这样的字节码:

public B();
  Code:
   0:   aload_0
   1:   invokespecial   #10; //Method java/lang/Object."<init>":()V
   4:   aload_0
   5:   ldc     #12; //int 86753098
   7:   anewarray       #3; //class java/lang/Object
   10:  putfield        #13; //Field temp:[Ljava/lang/Object;
   13:  return

当常量较大时,会使用操作码“ldc”,根据JVM文档,“一个无符号字节,必须是当前类运行时常量池中的有效索引”。在任何情况下,该常量都嵌入到B中。我想,在操作码中,您只能访问当前类的运行时常量池,因此将常量写入类文件的决定与实现无关(但我不确定)。

1
你可以使用JDK附带的javap工具从*.class文件中获取字节码。 - Jesper

4

哇 - 每天都会学到新东西!

来自Java规范...

注意:如果一个原始类型或字符串被定义为常量,并且在编译时已知其值,则编译器将在代码中的所有位置上替换常量名称为其值。这称为编译时常量。如果常量在外部世界中的值发生更改(例如,如果立法规定pi实际上应该是3.975),则需要重新编译使用此常量的任何类以获得当前值。


真的吗?替换一个jar文件意味着替换已编译的类。因此,该语句并未证实您的“未被替换”的说法。快速反编译类文件将揭示正确答案。 - Brent Worden

3

这里的重要概念是,static final字段是使用JLS中定义的编译时常量进行初始化的。如果使用非常量初始值(或非static或非final),则不会被复制:

public static final int SIZE = null!=null?0: 100;

(null不是一个*编译时常量。)


我认为创建一个虚拟的CONST()方法,然后执行"SIZE = CONST(100);"会更易于阅读。 https://dev59.com/6HI-5IYBdhLWcg3wwLS3#12065326 - Julius Musseau

2

实际上,我遇到过这种奇怪的情况。

这将直接将"100"编译到b类中。如果你只重新编译A类,这不会更新B类中的值。

此外,编译器可能不会注意到重新编译类b(当时我正在编译单个目录,而类B在一个单独的目录中,编译A的目录并没有触发B的编译)。


1
作为一种优化,编译器将内联该final变量。

因此,在编译时它看起来像这样。

class b
{
      Object[] temp = new Object[100];
}

0
需要注意的一点是:静态常量的值在编译时已知,如果该值在编译时不可知,则编译器不会将代码中的常量名称替换为其值。
  public class TestA {
      // public static final int value = 200;
      public static final int value = getValue();
      public static int getValue() {
        return 100;
      }
  }

public class TestB {
    public static void main(String[] args) {
        System.out.println(TestA.value);
    }
}

首先编译TestA和TestB,运行TestB

然后将TestA.getValue()更改为返回200,编译TestA,运行TestB,TestB将获得新值 输入图像描述


0

这里有一个例外:

如果在编译时静态 final 字段为 null,则它不会被替换为 null(实际上是它的值)

A.java

class A{
     public static final String constantString = null;
}

B.java

class B{
     public static void main(String... aa){
         System.out.println(A.constantString);
     }
}

编译 A.java 和 B.java 并运行 java B 输出将会是null
现在使用以下代码更新 A.java,并仅编译此类。
class A{
     public static final String constantString = "Omg! picking updated value without re-compilation";
}

现在运行 java B 输出将是哇!在不重新编译的情况下选择更新的值

-2
Java确实会优化这些类型的值,但仅当它们在同一个类中时才会进行优化。在这种情况下,JVM会查找A.SIZE而不是对其进行优化,因为它考虑了你所考虑的用例。

什么样的用例会导致在编译时替换值,而什么样的用例则不会替换值? - Without Me It Just Aweso
1
如果它是编译时常量,无论在哪个类中定义或使用,都会在编译时被替换,这是错误的。 - user85421

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