In Java we make things immutable with the final
keyword and there are at least 3 ways in which immutability can make some real difference in code performance. Those 3 points have their common origin in making the compiler or the developer doing better assumptions:
- More reliable code
- More performant code
- More efficient memory allocation and garbage collection
More reliable code
As stated by many of the other replies and comments, making classes immutable leads to cleaner and more maintainable code, and making objects immutable makes them easier to handle, because they can be in exactly one state, so this translates to easier concurrency and an optimization of the time needed to accomplish tasks.
Furthermore the compiler warns you about the usage of an uninitialized variable and won't let you reassign it with a new value.
If we talk about method parameters, declaring them final
would make the compiler complaint if you accidentally use the same name for a variable, or reassign it's value (making the parameter no more accessible).
More performant code
A simple analysis of the generated bytecode should put the last world to the performance question: using a minimally modified version of the code posted by @rustyx in his reply, you can see that the generated bytecode is different when the compiler knows that objects won't mutate their value.
That's the code:
public class FinalTest {
private static final int N_ITERATIONS = 1000000;
private static String testFinal() {
final String a = "a";
final String b = "b";
return a + b;
}
private static String testNonFinal() {
String a = "a";
String b = "b";
return a + b;
}
private static String testSomeFinal() {
final String a = "a";
String b = "b";
return a + b;
}
public static void main(String[] args) {
measure("testFinal", FinalTest::testFinal);
measure("testSomeFinal", FinalTest::testSomeFinal);
measure("testNonFinal", FinalTest::testNonFinal);
}
private static void measure(String testName, Runnable singleTest){
final long tStart = System.currentTimeMillis();
for (int i = 0; i < N_ITERATIONS; i++)
singleTest.run();
final long tElapsed = System.currentTimeMillis() - tStart;
System.out.printf("Method %s took %d ms%n", testName, tElapsed);
}
}
compiling it with openjdk17: javac FinalTest.java
then decompiling: javap -c -p FinalTest.class
lead to this bytecode:
private static java.lang.String testFinal();
Code:
0: ldc #7 // String ab
2: areturn
private static java.lang.String testNonFinal();
Code:
0: ldc #9 // String a
2: astore_0
3: ldc #11 // String b
5: astore_1
6: aload_0
7: aload_1
8: invokedynamic #13, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
13: areturn
private static java.lang.String testSomeFinal();
Code:
0: ldc #11 // String b
2: astore_0
3: aload_0
4: invokedynamic #17, 0 // InvokeDynamic #1:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;
9: areturn
// omitted bytecode for the measure() method, which is not interesting
As you can see, there are cases where the final
keyword makes a difference.
For the sake of completeness those are the measured times:
Method testFinal took 5 ms
Method testSomeFinal took 13 ms
Method testNonFinal took 20 ms
Those times seems to be irrelevant (given that we accomplished 1 million tasks), but I think that, after some while, JIT optimization is doing it's magic and smooths out the differences, but even with that said, 4x is not that negligible, considering that, when it comes to testNonFinal
turn, the JVM is warmed up by the previous tests, and the common code should yet be optimized.
Easier inlining
Less bytecode translates also to easier and shorter inlining, and so better usage of the resources and better performance.
Embedded devices
Java developers can potentially write code that runs on servers, desktops and small or embedded devices, so making code more efficient at compile time (and not completely relying on the JVM optimizations) can save memory, time and energy on all the runtimes and lead to less concurrency problems and errors.
More efficient memory allocation and garbage collection
If objects have final or immutable fields, their state can't change and the memory they need is more easily estimated when they are created (so this lead to fewer relocations) and need less defensive copies: in a getter I can directly share an immutable object, without creating a defensive copy.
Finally there's another point about future possibilities: when project Valhalla will see the sunlight and "value classes" will be available, having applied immutability to objects' fields will be a significant simpification for those who want to use them, and take advantage of numerous JIT-compiler optimizations that could came out.
A personal note about immutability
If variables, objects' properties and methods' parameters had been immutable by default in Java (like in Rust), the developers would have been forced to write cleaner and better performing code, and explicitly declaring mutable
all the objects which can mutate their value would have made developers more conscious about possible errors.
I don't know if for final class
es would be the same, because mutable class
does not sound that meaningful to me