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.