Search code examples
javafinal

How can removing final keyword change the way a program behaves?


This question is not primarily about Strings. Out of academic curiosity I would like to know how the final modifier on a variable can change the behaviour a program. The following example shows it is possible.

These lines print true

final String x = "x";
System.out.println(x + x == "xx");

but these lines print false

String x = "x";
System.out.println(x + x == "xx");

Apart from String interning, are there any other things that can cause the behaviour of a program to change if the modifier final is removed from a variable declaration? I am assuming that the program compiles with or without the modifier.

Please don't vote to close it as duplicate of Comparing strings with == which are declared final in Java. I understand the Strings example.

I'm asking if there are any other reasons removing a final modifier can make a difference. Please can someone link to an answer or answer the question. Thanks.


Solution

  • The final modifier only ensures that the variable is definitely assigned, and prohibits any reassignment to and from that variable.

    The only special cases that can be observed are expressly stated in the JLS:

    A variable of primitive type or type String, that is final and initialized with a compile-time constant expression (§15.28), is called a constant variable.

    Whether a variable is a constant variable or not may have implications with respect to class initialization (§12.4.1), binary compatibility (§13.1, §13.4.9) and definite assignment (§16).

    There's a decent amount of JLS reading, and to cover the main point: By JLS §13.4.9, you will not encounter any ill effects upon removing the final modifier.

    However, by JLS 17.5, if you rely on the guarantee of a thread only seeing the definitely assigned variables in an object that it can observe, then removing the final variable will cause those variables to no longer be visible to another thread.


    So, if we look at class initialization first, there are rules surrounding class initialization if the field is static and not a constant variable:

    A class or interface type T will be initialized immediately before the first occurrence of any one of the following:

    • T is a class and an instance of T is created.
    • T is a class and a static method declared by T is invoked.
    • A static field declared by T is assigned.
    • A static field declared by T is used and the field is not a constant variable (§4.12.4).

    In JLS §13.1, it is spelled out that changing a field to final can break binary compatibility:

    References to fields that are constant variables (§4.12.4) are resolved at compile time to the constant value that is denoted. No reference to such a field should be present in the code in a binary file (except in the class or interface containing the field, which will have code to initialize it). Such a field must always appear to have been initialized (§12.4.2); the default initial value for the type of such a field must never be observed. See §13.4.9 for a discussion.

    From 13.4.9:

    If a field that was not declared final is changed to be declared final, then it can break compatibility with pre-existing binaries that attempt to assign new values to the field.

    Deleting the keyword final or changing the value to which a field is initialized does not break compatibility with existing binaries.

    If a field is a constant variable (§4.12.4), then deleting the keyword final or changing its value will not break compatibility with pre-existing binaries by causing them not to run, but they will not see any new value for the usage of the field unless they are recompiled. This is true even if the usage itself is not a compile-time constant expression (§15.28).

    This result is a side-effect of the decision to support conditional compilation, as discussed at the end of §14.21.

    So from that alone, be careful about suddenly changing fields to final. Removing the field is safe.

    ...but that only applies to a single-threaded world. From JLS 17.5:

    Fields declared final are initialized once, but never changed under normal circumstances. The detailed semantics of final fields are somewhat different from those of normal fields. In particular, compilers have a great deal of freedom to move reads of final fields across synchronization barriers and calls to arbitrary or unknown methods. Correspondingly, compilers are allowed to keep the value of a final field cached in a register and not reload it from memory in situations where a non-final field would have to be reloaded.

    final fields also allow programmers to implement thread-safe immutable objects without synchronization. A thread-safe immutable object is seen as immutable by all threads, even if a data race is used to pass references to the immutable object between threads. This can provide safety guarantees against misuse of an immutable class by incorrect or malicious code. final fields must be used correctly to provide a guarantee of immutability.

    An object is considered to be completely initialized when its constructor finishes. A thread that can only see a reference to an object after that object has been completely initialized is guaranteed to see the correctly initialized values for that object's final fields.

    So, if your program relies on the above guarantee for it to function normally, then removing the final keyword will have consequences in threading.