First, a puzzle: What does the following code print?
public class RecursiveStatic {
public static void main(String[] args) {
System.out.println(scale(5));
}
private static final long X = scale(10);
private static long scale(long value) {
return X * value;
}
}
Answer:
0
Spoilers below.
If you print X
in scale(long) and redefine X = scale(10) + 3
,
the prints will be X = 0
then X = 3
.
This means that X
is temporarily set to 0
and later set to 3
.
This is a violation of final
!
The static modifier, in combination with the final modifier, is also used to define constants. The final modifier indicates that the value of this field cannot change.
Source: https://docs.oracle.com/javase/tutorial/java/javaOO/classvars.html [emphasis added]
My question:
Is this a bug?
Is final
ill-defined?
Here is the code that I am interested in.
X
is assigned two different values: 0
and 3
.
I believe this to be a violation of final
.
public class RecursiveStatic {
public static void main(String[] args) {
System.out.println(scale(5));
}
private static final long X = scale(10) + 3;
private static long scale(long value) {
System.out.println("X = " + X);
return X * value;
}
}
This question has been flagged as a possible duplicate of Java static final field initialization order.
I believe that this question is not a duplicate since
the other question addresses the order of initialization while
my question addresses a cyclic initialization combined with the final
tag.
From the other question alone, I would not be able to understand why the code in my question does not make an error.
This is especially clear by looking at the output that ernesto gets:
when a
is tagged with final
, he gets the following output:
a=5
a=5
which does not involve the main part of my question: How does a final
variable change its variable?
Reading an uninitialized field of an object ought to result in a compilation error. Unfortunately for Java, it does not.
I think the fundamental reason why this is the case is "hidden" deep within the definition of how objects are instantiated and constructed, though I don't know the details of the standard.
In a sense, final is ill-defined because it doesn't even accomplish what its stated purpose is due to this problem. However, if all your classes are properly written, you don't have this problem. Meaning all fields are always set in all constructors and no object is ever created without calling one of its constructors. That seems natural until you have to use a serialization library.
Class level members can be initialized in code within the class definition. The compiled bytecode cannot initialize the class members inline. (Instance members are handled similarly, but this is not relevant for the question provided.)
When one writes something like the following:
The bytecode generated would be similar to the following:
The initialization code is placed within a static initializer which is run when the class loader first loads the class. With this knowledge, your original sample would be similar to the following:
scale(10)
to assign thestatic final
fieldX
.scale(long)
function runs while the class is partially initialized reading the uninitialized value ofX
which is the default of long or 0.0 * 10
is assigned toX
and the class loader completes.scale(5)
which multiplies 5 by the now initializedX
value of 0 returning 0.The static final field
X
is only assigned once, preserving the guarantee held by thefinal
keyword. For the subsequent query of adding 3 in the assignment, step 5 above becomes the evaluation of0 * 10 + 3
which is the value3
and the main method will print the result of3 * 5
which is the value15
.A very interesting find. To understand it we need to dig into the Java Language Specification (JLS).
The reason is that
final
only allows one assignment. The default value, however, is no assignment. In fact, every such variable (class variable, instance variable, array component) points to its default value from the beginning, before assignments. The first assignment then changes the reference.Class variables and default value
Take a look at the following example:
We did not explicitly assign a value to
x
, though it points tonull
, it's default value. Compare that to §4.12.5:Note that this only holds for those kind of variables, like in our example. It does not hold for local variables, see the following example:
From the same JLS paragraph:
Final variables
Now we take a look at
final
, from §4.12.4:Explanation
Now coming back to the your example, slightly modified:
It outputs
Recall what we have learned. Inside the method
assign
the variableX
was not assigned a value to yet. Therefore, it points to its default value since it is an class variable and according to the JLS those variables always immediately point to their default values (in contrast to local variables). After theassign
method the variableX
is assigned the value1
and because offinal
we can't change it anymore. So the following would not work due tofinal
:Example in the JLS
Thanks to @Andrew I found a JLS paragraph that covers exactly this scenario, it also demonstrates it.
But first let's take a look at
Why is this not allowed, whereas the access from the method is? Take a look at §8.3.3 which talks about when accesses to fields are restricted if the field was not initialized yet.
It lists some rules relevant for class variables:
It's simple, the
X = X + 1
is caught by those rules, the method access not. They even list this scenario and give an example:Nothing to do with final here.
Since it is at instance or class level, it holds the default value if nothing gets assigned yet. That is the reason you seeing
0
when you accessing it without assigning.If you access
X
without completely assigning, it holds the default values of long which is0
, hence the results.It's not a bug at all, simply put it is not an illegal form of forward references, nothing more.
It's simply allowed by the Specification.
To take your example, this is exactly where this matches:
You are doing a forward reference to
scale
that is not illegal in any way as said before, but allows you to get the default value ofX
. again, this is allowed by the Spec (to be more exact it is not prohibited), so it works just fineNot a bug.
When the first call to
scale
is called fromIt tries to evaluate
return X * value
.X
has not been assigned a value yet and therefore the default value for along
is used (which is0
).So that line of code evaluates to
X * 10
i.e.0 * 10
which is0
.