First, a puzzle: What does the following code print?
public class RecursiveStatic {
public static void main(String[] args) {
System.out.println(scale
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.
Take a look at the following example:
private static Object x;
public static void main(String[] args) {
System.out.println(x); // Prints 'null'
}
We did not explicitly assign a value to x
, though it points to null
, it's default value. Compare that to §4.12.5:
Initial Values of Variables
Each class variable, instance variable, or array component is initialized with a default value when it is created (§15.9, §15.10.2)
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:
public static void main(String[] args) {
Object x;
System.out.println(x);
// Compile-time error:
// variable x might not have been initialized
}
From the same JLS paragraph:
A local variable (§14.4, §14.14) must be explicitly given a value before it is used, by either initialization (§14.4) or assignment (§15.26), in a way that can be verified using the rules for definite assignment (§16 (Definite Assignment)).
Now we take a look at final
, from §4.12.4:
final Variables
A variable can be declared final. A final variable may only be assigned to once. It is a compile-time error if a final variable is assigned to unless it is definitely unassigned immediately prior to the assignment (§16 (Definite Assignment)).
Now coming back to the your example, slightly modified:
public static void main(String[] args) {
System.out.println("After: " + X);
}
private static final long X = assign();
private static long assign() {
// Access the value before first assignment
System.out.println("Before: " + X);
return X + 1;
}
It outputs
Before: 0
After: 1
Recall what we have learned. Inside the method assign
the variable X
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 the assign
method the variable X
is assigned the value 1
and because of final
we can't change it anymore. So the following would not work due to final
:
private static long assign() {
// Assign X
X = 1;
// Second assign after method will crash
return X + 1;
}
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
private static final long X = X + 1;
// Compile-time error:
// self-reference in initializer
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:
For a reference by simple name to a class variable
f
declared in class or interfaceC
, it is a compile-time error if:
The reference appears either in a class variable initializer of
C
or in a static initializer ofC
(§8.7); andThe reference appears either in the initializer of
f
's own declarator or at a point to the left off
's declarator; andThe reference is not on the left hand side of an assignment expression (§15.26); and
The innermost class or interface enclosing the reference is
C
.
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:
Accesses by methods are not checked in this way, so:
class Z { static int peek() { return j; } static int i = peek(); static int j = 1; } class Test { public static void main(String[] args) { System.out.println(Z.i); } }
produces the output:
0
because the variable initializer for
i
uses the class method peek to access the value of the variablej
beforej
has been initialized by its variable initializer, at which point it still has its default value (§4.12.5).
It's not a bug at all, simply put it is not an illegal form of forward references, nothing more.
String x = y;
String y = "a"; // this will not compile
String x = getIt(); // this will compile, but will be null
String y = "a";
public String getIt(){
return y;
}
It's simply allowed by the Specification.
To take your example, this is exactly where this matches:
private static final long X = scale(10) + 3;
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 of X
. again, this is allowed by the Spec (to be more exact it is not prohibited), so it works just fine
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:
public class Demo1 {
private static final long DemoLong1 = 1000;
}
The bytecode generated would be similar to the following:
public class Demo2 {
private static final long DemoLong2;
static {
DemoLong2 = 1000;
}
}
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:
public class RecursiveStatic {
private static final long X;
private static long scale(long value) {
return X * value;
}
static {
X = scale(10);
}
public static void main(String[] args) {
System.out.println(scale(5));
}
}
scale(10)
to assign the static final
field X
.scale(long)
function runs while the class is partially initialized reading the uninitialized value of X
which is the default of long or 0.0 * 10
is assigned to X
and the class loader completes.scale(5)
which multiplies 5 by the now initialized X
value of 0 returning 0.The static final field X
is only assigned once, preserving the guarantee held by the final
keyword. For the subsequent query of adding 3 in the assignment, step 5 above becomes the evaluation of 0 * 10 + 3
which is the value 3
and the main method will print the result of 3 * 5
which is the value 15
.
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 is 0
, hence the results.
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.
Not a bug.
When the first call to scale
is called from
private static final long X = scale(10);
It tries to evaluate return X * value
. X
has not been assigned a value yet and therefore the default value for a long
is used (which is 0
).
So that line of code evaluates to X * 10
i.e. 0 * 10
which is 0
.