I am amazed that no one has actually posted some real code that is de-compiled to prove that there is at least some minor difference.
For the reference this has been tested against javac
version 8
, 9
and 10
.
Suppose this method:
public static int test() {
Object left = new Object();
Object right = new Object();
return left.hashCode() + right.hashCode();
}
Compiling this code as it is, produces the exact same byte code as when final
would have been present (final Object left = new Object();
).
But this one:
public static int test() {
int left = 11;
int right = 12;
return left + right;
}
Produces:
0: bipush 11
2: istore_0
3: bipush 12
5: istore_1
6: iload_0
7: iload_1
8: iadd
9: ireturn
Leaving final
to be present produces:
0: bipush 12
2: istore_1
3: bipush 11
5: iload_1
6: iadd
7: ireturn
The code is pretty much self-explanatory, in case there is a compile time constant, it will be loaded directly onto the operand stack (it will not be stored into local variables array like the previous example does via bipush 12; istore_0; iload_0
) - which sort of makes sense since no one can change it.
On the other hand why in the second case the compiler does not produce istore_0 ... iload_0
is beyond me, it's not like that slot 0
is used in any way (it could shrink the variables array this way, but may be Im missing some internals details, can't tell for sure)
I was surprised to see such an optimization, considering how little ones javac
does. As to should we always use final
? I'm not even going to write a JMH
test (which I wanted to initially), I am sure that the diff is in the order of ns
(if possible to be captured at all). The only place this could be a problem, is when a method could not be inlined because of it's size (and declaring final
would shrink that size by a few bytes).
There are two more final
s that need to be addressed. First is when a method is final
(from a JIT
perspective), such a method is monomorphic - and these are the most beloved ones by the JVM
.
Then there are final
instance variables (that must be set in every constructor); these are important as they will guarantee a correctly published reference, as touched a bit here and also specified exactly by the JLS
.
That being said : there is one more thing that is invisible to every single answer here: garbage collection
. It is going to take a lot of time to explain, but when you read a variable, a GC
has a so-called barrier
for that read. Every aload
and getField
is "protected" via such a barrier, a lot more details here. In theory, final
fields do not need such a "protection" (they can skip the barrier entirely). So if a GC does that - final
will improve performance.