带有静态和最终限定符的奇怪 Java 行为

在我们的团队中,我们发现了一些奇怪的行为,我们同时使用了 staticfinal限定词。这是我们的测试课:

public class Test {


public static final Test me = new Test();
public static final Integer I = 4;
public static final String S = "abc";


public Test() {
System.out.println(I);
System.out.println(S);
}


public static Test getInstance() { return me; }


public static void main(String[] args) {
Test.getInstance();
}
}

当我们运行 main方法时,我们得到的结果是:

null
abc

如果它两次都写 null值,我会理解,因为静态类成员的代码是从上到下执行的。

有人能解释一下为什么会发生这种行为吗?

4116 次浏览

S is a compile-time constant, following the rules of JLS 15.28. So any occurrence of S in the code is replaced with the value which is known at compile-time.

If you change the type of I to int, you'll see the same for that, too.

These are the steps taken when you run your program:

  1. Before main can be run, the Test class must be initialized by running static initializers in order of appearance.
  2. To initialize the me field, start executing new Test().
  3. Print the value of I. Since the field type is Integer, what seems like a compile-time constant 4 becomes a computed value (Integer.valueOf(4)). The initializer of this field has not yet run, printing the initial value null.
  4. Print the value of S. Since it is initialized with a compile-time constant, this value is baked into the referencing site, printing abc.
  5. new Test() completes, now the initializer for I executes.

Lesson: if you rely on eagerly initialized static singletons, place the singleton declaration as the last static field declaration, or resort to a static initializer block that occurs after all other static declarations. That will make the class appear fully initialized to the singleton's construction code.

You have strange behavior due to the Integer data type. Regarding JLS 12.4.2 static fields are initialized in the order you write it, BUT compile-time constants are initialized first.

If you do not use the wrapper type Integer but the int type, you get the behavior you want.

Your Test compiles into:

public class Test {


public static final Test me;
public static final Integer I;
public static final String S = "abc";


static {
me = new Test();
I = Integer.valueOf(4);
}


public Test() {
System.out.println(I);
System.out.println("abc");
}


public static Test getInstance() { return me; }


public static void main(String[] args) {
Test.getInstance();
}
}

As you can see, the constructor for Test gets called before I is initialized. This is why it prints "null" for I. If you were to swap the declaration order for me and I, you would get the expected result because I would be initialized before the constructor is invoked. You can also change the type for I from Integer to int.

Because 4 needs to get autoboxed (i.e., wrapped in an Integer object), it is not a compile-time constant and is part of the static initializer block. However, if the type were int, the number 4 would be a compile-time constant, so it would not need to be explicitly initialized. Because "abc" is a compile-time constant, the value of S is printed as expected.

If you would replace,

public static final String S = "abc";

with,

public static final String S = new String("abc");

Then you would notice the output of S is "null" as well. Why does that happen? For the same reason why I also outputs "null". Fields like these that have literal, constant values (that do not need autoboxing, like String) are attributed with the "ConstantValue" attribute when compiled, which means that their value can be resolved simply by looking into the class' constant pool, without needing to run any code.