When should you use the Java “final” keyword?
In this post I would like to go into some detail on the purpose of the final
keyword and in which contexts I consider
using it and why. It is important to note that I'm working as a Java developer in an enterprise context, so the best
practices I outline below, apply to that context and might need to be applied differently when you're working as a
developer on a framework or library.
Knowledge of Java, its approach to inheritance and its multi-threading model is recommended before reading this post.
Contents
- The final keyword in a couple of lines
- Immutability of state
- Immutability of design
- Thread-safety and final
- Effectively final
- The use of the final keyword
- Final for method parameters
- Final for local variables
- Final fields
- Final and lambda expressions
- Final and try-with-resources
- Final methods
- Final classes
- Dependency injection
- Java 16 Records
- References
The final keyword in a couple of lines
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
Java 8 Specification (https:
// )docs .oracle .com /javase /specs /jls /se8 /html /jls -4 .html #jls -4 .12 .4
In other words: A final variable must be assigned before it is first read, and it can never be reassigned after this point
Additionally, the final
keyword (or modifier) can be used in a method signature to prevent the method being overridden,
or in a class definition, in order to prevent inheritance from that class.
Immutability of state
As explained above, the final
keyword is used to prevent change. The first kind of change that it can be used to
prevent is 'change in state'. For primitive fields, primitive method parameters and primitive fields this is rather
straightforward.
public class Example {
private static final int aNumber = 10;
public static void main(String[] args) {
aNumber = 20; //Does not compile, since aNumber is final
}
}
In the example above, the field aNumber
is immutable, meaning it can no longer be changed to any other value after
its initial assignment.
It becomes more complex when fields, parameters or variables referencing an object are marked as final. In this case the reference to the object can not be changed, but the object itself can. In order to ensure that the object itself is safe from change, it needs to be an instance of an 'immutable class'.
public class Example {
private static final AtomicInteger aNumber = new AtomicInteger(10);
public static void main(String[] args) {
aNumber.getAndIncrement();
System.out.println("aNumber is now " + aNumber.get()); //Will print 'aNumber is now 11'
}
}
An 'immutable class' should adhere, at least, to the following rules:
- The class itself should be marked as final
- The class' fields need to be final, so the values can't be changed
- The types of the class' fields should be instances of 'immutable classes' as well
public final class ImmutableClass {
private final int counter;
public ImmutableClass(int counter) {
this.counter = counter;
}
public int getCounter() {
return this.counter;
}
}
Immutability of design
The second kind of change that is prevented by the final
keyword, is 'change in design'. This kind of change is, in a
lot of cases, important when developing a library or framework. If you want to prevent modifications being made to the
design of the class, by library or framework consumers, you should mark that class as final to prevent inheritance.
The same principle applies when you want to protect a method implementation being changed by that method being overridden in a subclass. In this case, you would mark the method as final in the parent class to prevent its implementation being changed by a subclass.
In enterprise applications, I have not seen final
being used that often to prevent changes in design.
Thread-safety and final
When you are writing a piece of code that will be used from a multi-threaded context (basically any web application), the final keyword can be a valuable tool in preventing programming errors. Since a final variable can not be reassigned you are guaranteed that multiple threads will see the same value, when accessing that variable.
It is important to note that change in state can only be prevented if both the variable and its type are immutable. If this is not the case, then the internal state of the variable can still be modified, which can result in race conditions if that variable is used in a multi-threaded context.
Note that most of the above applies only to instance variables and not to local variables, which are thread-safe by default.
Effectively final
A variable is 'effectively final' when the JVM, at compile-time, notices that a variable is never reassigned, after its initial assignment.
From the specification:
A variable or parameter whose value is never changed after it is initialized is effectively final.
Java 8 Specification (https:
// )docs .oracle .com /javase /specs /jls /se8 /html /jls -4 .html #jls -4 .12 .4
The use of the final keyword
Performance
Although in certain JVM distributions, the final
keyword seems to result in some potential minor performance
improvements, I do not consider performance a relevant factor when deciding whether to use the keyword.
Immutable classes for use in a multi-threaded environment
When writing a class that will be used from a multi-threaded context, it is important to ensure that all the fields use classes that are thread-safe. This means that those classes are either immutable, or they are designed to take thread-safety into account.
In case a class is used that is not thread-safe, we need to ensure thread-safety ourselves by using the synchronized
keyword, or the appropriate locking mechanism.
Fake immutability
Whenever you use the final
keyword, you should be careful not to introduce 'fake immutability'. This happens when you
mark a variable at any level as final, but the variable is non-primitive, and the type of the variable is not immutable.
In this scenario, care should be taken that it is clear to the readers of this code, that the given variable 'is'
mutable.
Final for method parameters
public class Example {
public int sum(final int a, final int b) {
return a + b;
}
}
In my opinion, the fact that you are able to reassign 'input' parameters, is a design flaw in Java. There are two distinct cases here: reusing a primitive input parameter and modifying an input parameter that is an Object.
Reusing a primitive input parameter
public class Example {
public static void main(String[] args) {
int value = 20;
value = incrementValueByTenTwice(value);
System.out.println("Value is " + value);
}
public static int incrementValueByTenTwice(int value) {
value += 10;
value += 10;
return value;
}
}
In the example above, we are reusing the input parameter value
in order to increment an input value by 10 twice. It's
important to note, since Java always passes parameters by value, reusing the input parameter, does not change the value
of the local variable defined in the main
method.
In almost all cases, however, there is little value in reusing an input parameter like this. It can even confuse developers that are new to the Java language and do not fully understand pass-by-value semantics. A cleaner and more readable solution would be to introduce a local variable, as in the example below.
public class Example {
public static void main(String[] args) {
int value = 20;
value = incrementValueByTenTwice(value);
System.out.println("Value is " + value);
}
public static int incrementValueByTenTwice(int value) {
int incrementedValue = value + 10;
incrementedValue += 10;
return incrementedValue;
}
}
In the example above, there is little need to make value
a final parameter. It adds additional clutter to the method
signature, and you should adopt the best practice of never reassigning parameters, as it impacts code readability.
Modifying an input parameter that is an Object
public class Example {
public static void main(String[] args) {
AtomicInteger oldValue = new AtomicInteger(20);
AtomicInteger newValue = incrementValueByTenTwice(oldValue);
System.out.println("Old value is " + oldValue);
System.out.println("New value is " + newValue);
}
public static AtomicInteger incrementValueByTenTwice(AtomicInteger value) {
value.addAndGet(10);
value.addAndGet(10);
return value;
}
}
When reusing input parameters that are Objects, extra care should be taken. In the above example, the value 40 will be printed twice. This is due to the fact that even though Java passes a parameter by value, it's the reference to the object that is being passed, rather than a complete copy of the object. This means that when mutating state on the referenced object, you are still changing the state on the outer object as well.
Marking the input parameter as final in this case, can even add to the confusion, as it can give the impression that no state can be modified, but in reality marking the parameter as final, just prevents you from changing the object being referenced by the parameter, but not from changing the state of the referenced object. For this reason, and the reasons already mentioned earlier on, I would never mark input parameters as final.
Final for local variables
For local variables, you should use the final
keyword with care. As with method parameters, the final
keyword does
not improve readability and offers little semantic value.
Sometimes the use of the final
keyword is recommended as a tool to improve readability. In a longer method it can
indicate to a reader that a variable will not be reassigned at a later stage. This does not mean, however, that the
variable's internal state can not change. It is often better to split the long method into several smaller ones, as
this will benefit readability much more than adding a keyword.
Another often-used argument is that you should make local variables final, because it can prevent mistakes. Again, this argument usually applies when writing code for a longer method, and it is better to split the method into several smaller ones.
When writing methods the final
keyword adds clutter, which often reduces the readability of those methods. It is
better to keep your methods small and give clear names to your local variables. Taking into account small methods and
clear names, there is little value to be found in using the final
keyword on local variables.
Final fields
For instance variables (or fields), the final
keyword has a high semantic value, but, more importantly, if used
correctly, it can help avoid many thread-safety pitfalls. In general, as recommended in Joshua Bloch's Effective
Java, every field should be marked as final by default.
When marking a field as being final, it is important to double-check that the field is using either an 'immutable class', or it is of a type that has strong thread-safety guarantees. If neither of these two conditions is met, it is often better to keep the field non-final, as this makes it clear to readers of your code, that care should be taken when writing code that uses this specific field.
It's important to never assume that a type from an external library is thread-safe by default. Always check the documentation to learn about any potential pitfalls when using a type from a multi-threaded context.
Final and lambda expressions
Local variables defined outside the scope of a Lambda function need to be either marked as final, or need to be 'effectively final'. This does not apply to instance variables (fields), these do not need to be final to be used in Lambda expressions.
class DoesNotCompile {
public static void main(String[] args) {
int valueToAdd = 10;
valueToAdd += 20; //This addition, causes the field to no longer be 'effectively final'
Set<Integer> incrementedValues = Set.of(1, 2, 3).stream()
.map(number -> number + valueToAdd) //Will not compile due to variable not being 'effectively final'
.collect(Collectors.toUnmodifiableSet());
System.out.println(incrementedValues);
}
}
The reason that local variables need to be, at least, 'effectively final' is due to language design choices and Java's memory model.
The first reason has to do with the scope of local variables. A choice was made to never make a local variable outlive the scope of the method in which it was defined. Since Lambda expressions can live longer than its parent's scope, because they are evaluated lazily, this would violate that principle. In some languages, such as C# and JavaScript, a local variable can outlive its parent scope. The idea is that not allowing local variables to be changed after method execution will prevent programming errors from being made.
It's important to note that this limitation only applies to local variables and not to instance variables, or fields. This has to do with how Java internally organises memory allocation. Local variables are stored in the Stack, while instance variables are stored in Java's Heap Space. It was always possible to modify instance variables from a different thread than the current one, but for local variables this would have required a change to where the variable is stored. It would need to be 'lifted' from the Stack into the Heap.
Due to the fact that instance variables can still be modified from within a Lambda expression, you still need to make sure that thread-safety is taken into account when using such a variable within a Lambda expression.
Final and try-with-resources
Java 8
If you want to use the auto-close functionality on a resource within the 'try' statement of a 'try-with-resources'
block, then the variable needs to be defined in the 'try' statement itself. There is no need to add the final
keyword
to the declaration, as the variable is marked as final implicitly.
public class ThisWorks {
public void openAndCloseStream() {
try (InputStream finalStream = new FileInputStream(new File("example.txt"))) {
// Do work on stream
}
}
}
In the above example, reassigning finalStream
will give a compile-time error.
public class ThisDoesNotCompile {
public void openAndCloseStream() {
InputStream stream = new FileInputStream(new File("example.txt"));
try (stream) {
// Do work on stream
}
}
}
Java 9
Auto-closeable variables that are 'effectively final' can be used in a 'try-with-resources' statement, even if they are defined outside its scope.
public class ThisWorksInJava9 {
public void openAndCloseStream() {
InputStream stream = new FileInputStream(new File("example.txt"));
try (stream) {
// Do work on stream
}
}
}
Final methods
A method in a class can be marked as final in order to prevent it being overridden and changed.
You might wish to make a method final if it has an implementation that should not be changed and it is critical to the consistent state of the object.
Oracle - Writing final classes and methods (https:
// )docs .oracle .com /javase /tutorial /java /IandI /final .html
Methods called from constructors should generally be declared final. If a constructor calls a non-final method, a subclass may redefine that method with surprising or undesirable results.
Oracle - Writing final classes and methods (https:
// )docs .oracle .com /javase /tutorial /java /IandI /final .html
Marking a method as final is usually a design choice and should be done with care, as it could violate the Open-Closed Principle. In practice, when writing enterprise applications, there is seldom a need to make a method final. It could be useful to mark a rare method as being final in order to warn fellow developers that you didn't expect it to be overridden.
Final classes
As with final methods, final classes can be useful when you know or believe that your classes may be used by external consumers with no access to the source code of that class, or to act as a warning sign to fellow developers working on the source code of that class indicating that you didn't expect the class to be inherited from.
Dependency injection
When writing code within the context of a dependency injection framework, a debate can arise, whether the injected dependencies should be marked as final. My own preference here is to never mark such dependencies as being final. There are a couple of reasons for this: dependency injection frameworks make heavy use of reflection, and the scope of the dependency being injected determines its mutability.
In case a final field is initialised to a compile-time constant, then modifying that field with reflection will not modify the actual uses of the value. When using a dependency-injection framework, this restriction does not apply. So, even though the field is marked as final, it can still be modified at a later point by the dependency injection container.
Secondly, when an injected dependency is marked as final, but its scope is not 'singleton', then the
dependency-injection container injects a proxy object, which can change internally, even though the field itself has
been marked as final. In this case the final
keyword can give a false sense of security.
Dependency injection is one of the rare cases, where you can deviate from the rule that you should by default mark all fields as final.
Java 16 Records
With Java 16's Records, the final
keyword is not required, as these are final classes with immutable fields by
default. A record can still have an instance field that is of a type that is not an 'immutable class', so records
are not by default 'immutable classes'.
Another interesting fact is that record classes retain their immutability at run-time and not only compile-time (as
happens when the final
keyword is used).
class MutableClass {
private int mutableInt;
public MutableClass(int mutableInt) {
this.mutableInt = mutableInt;
}
public int getMutableInt() {
return mutableInt;
}
public void setMutableInt(int mutableInt) {
this.mutableInt = mutableInt;
}
}
record MutableRecord(MutableClass mutableClass) {}
class Example {
public static void main(String[] args) {
MutableClass mutableClass = new MutableClass(15);
MutableRecord record = new MutableRecord(mutableClass);
System.out.println(record.mutableClass().getMutableInt());
mutableClass.setMutableInt(20);
System.out.println(record.mutableClass().getMutableInt());
}
}
References
Books
- Effective Java by Joshua Bloch (https:
// )www .amazon .com /Effective -Java -Joshua -Bloch /dp /0134685997
Links
- Java 8 specification (https:
// )docs .oracle .com /javase /specs /jls /se8 /html /jls -4 .html #jls -4 .12 .4 - Difference between final and effectively final (https:
// )stackoverflow .com /questions /20938095 /difference -between -final -and -effectively -final - Does use of the final keyword improve performance (https:
// )stackoverflow .com /questions /4279420 /does -use -of -final -keyword -in -java -improve -the -performance - Java final variables and performance (https:
// )stackoverflow .com /questions /30524582 /java -final -variables -and -performance - The final word on the final keyword (https:
// )web .archive .org /web /20050212033242 /http: // renaud .waldura .com /doc /java /final -keyword .shtml - In Java, should I use “final” for parameters and locals even when I don't have to? (https:
// )softwareengineering .stackexchange .com /questions /48413 /in -java -should -i -use -final -for -parameters -and -locals -even -when -i -dont -have -t - Excessive use “final” keyword in Java (https:
// )softwareengineering .stackexchange .com /questions /98691 /excessive -use -final -keyword -in -java - Why declare final variables inside methods? (https:
// )softwareengineering .stackexchange .com /questions /115690 /why -declare -final -variables -inside -methods ?noredirect = 1 & lq = 1 - Why don't instance fields need to be final or effectively final to be used in lambda expressions? (https:
// )stackoverflow .com /questions /67065119 /why -dont -instance -fields -need -to -be -final -or -effectively -final -to -be -used -in -la - Effectively final variables in try-with-resources in Java 9 (https:
// )www .tutorialspoint .com /effectively -final -variables -in -try -with -resources -in -java -9 - Open-closed principle (https:
// )en .wikipedia .org /wiki /Open %E2 %80 %93closed _principle - Changing private final fields via reflection (https:
// )stackoverflow .com /questions /4516381 /changing -private -final -fields -via -reflection - Why the restriction on local variable capture? (http:
// )www .lambdafaq .org /what -are -the -reasons -for -the -restriction -to -effective -immutability / - Records and Pattern Matching for Instanceof Finalized in JDK 16 (https:
// )www .infoq .com /news /2020 /08 /java16 -records -instanceof / - Java 14 Feature Spotlight: Records (https:
// )www .infoq .com /articles /java -14 -feature -spotlight / - Java 16 and IntelliJ IDEA (https:
// )blog .jetbrains .com /idea /2021 /03 /java -16 -and -intellij -idea /