Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is final ill-defined?

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?

like image 784
Little Helper Avatar asked Oct 14 '22 02:10

Little Helper


People also ask

What does it mean to be ill-defined?

Definition of ill-defined : not easy to see or understand The property's borders are ill-defined. an ill-defined mission.

What does ill-defined mean in medical terms?

poorly defined; not clear or definite.

What does it mean to be fatally ill?

: having a disease that cannot be cured and will cause death.

What is a terminal illness medical term?

What is a terminal condition? A terminal condition or illness is one that is life-limiting. In the near future it is expected the illness will result in permanent unconsciousness from which the person is unlikely to recover or death.


2 Answers

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:

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)).


Final variables

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)).


Explanation

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;
}

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

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 interface C, it is a compile-time error if:

  • The reference appears either in a class variable initializer of C or in a static initializer of C (§8.7); and

  • The reference appears either in the initializer of f's own declarator or at a point to the left of f's declarator; and

  • The 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 variable j before j has been initialized by its variable initializer, at which point it still has its default value (§4.12.5).

like image 217
Zabuzard Avatar answered Oct 16 '22 15:10

Zabuzard


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.

like image 23
Suresh Atta Avatar answered Oct 16 '22 16:10

Suresh Atta