In our team we found some strange behaviour where we used both static
and final
qualifiers. This is our test class:
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();
}
}
When we run the main
method, we get a result of:
null
abc
I would understand if it wrote null
values both times, since the code of static class members is executed from top to bottom.
Can anyone explain why this behaviour is happening?
These are the steps taken when you run your program:
main
can be run, the Test
class must be initialized by running static initializers in order of appearance.me
field, start executing new Test()
.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
.S
. Since it is initialized with a compile-time constant, this value is baked into the referencing site, printing abc
.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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With