final class Payment {
}
The final
keyword in Java might seem simple at first glance, but it’s a powerful tool that can significantly
impact our code’s design, readability, and performance.
In this article, we’ll delve into the different ways we can use final
and explore its various benefits for
Java developers.
final
keyword in Java?To put it simple, we say something is final when it can’t be "modified".
Imagine a chick that just hatched out of an egg. That egg is broken, and nothing can revert that; nobody can put together a hatched, or broken, egg, and make the small chick live inside it again, right? It’s the same in Java when something is final. However, technically speaking, the meaning that something can’t be modified takes several forms.
For example, we can use it when we have a variable holding a reference to an object and we want that variable to keep the same reference throughout the whole program execution. This means no other reference can be assigned to it.
But it can get tricky sometimes. Let’s see it in details.
final
keywordThe final
keyword can be applied in several scenarios. Let’s analyze them, one by one.
To declare a class final we use the final
class modifier. For example:
final class Payment {
}
A final class can’t be extended. This means no other class can be a subclass of the final class.
This is useful in situations where we want to limit the inheritance of our type (class), for example, for security reasons. It enforces the intended design and prevents unintended modifications through inheritance.
To declare a field final, again we use the same keyword. For example, if we were to add a final field ("amount") to our previous class, this could be it:
final class Payment {
private final BigDecimal amount;
Payment(BigDecimal amount) {
this.amount = amount;
}
}
From now on, this field (amount
) can’t be assigned a new value, or a compile-time error will be reported
and the code will fail to compile.
It’s also important to have in mind that a For example, suppose we have a field like this: |
In the previous Payment
class example, notice, how we had to declare our own constructor
to accept a value for our amount
field.
This is because all final instance fields must be initialized when the class instance is created.
In the case of the static ones, they must be initialized when the class is loaded.
Other ways to initialize final fields are as follows:
On the same line where the field is declared. For example:
final class Payment {
private static final BigDecimal MAX_ALLOWED = new BigDecimal(1000_000);
private final BigDecimal amount = new BigDecimal(100);
}
Inside an instance, or static initializer, respectively. For example:
final class Payment {
private static final BigDecimal MAX_ALLOWED;
private final BigDecimal amount;
static {
MAX_ALLOWED = new BigDecimal(1000_000);
}
{
amount = new BigDecimal(100);
}
}
For the sake of completeness, let’s mention here the third option: inside the class constructor (as we saw previously).
Final fields give us the big benefit of thread safety
(§17.5).
This doesn’t mean that if we declare all fields as final
in a class that will make the class thread safe;
but it’s one of the actions we can take towards that goal, and it’s a simple one.
It brings also with it more predictable and easy-to-reason-about code and the JVM can also perform low-level optimization over those fields because of their final nature.
One common scenario where final fields are used is when combined with the static keyword to create inmutable constants that we can use throughout our code.
To declare a final method we use the same final
keyword. For example:
class Payment {
private final BigDecimal amount = new BigDecimal(100);
final BigDecimal getAmount() {
return amount;
}
}
In this previous example the Payment
class is not final
.
This means it can be subclassed and its methods overridden or hidden in the children classes
(§8.4.3.3).
One way to avoid that is to declare those methods as final
.
This is a powerful technique: it permits that we have a class that can be extended but certain public behaviors of it can’t be changed, not even by its subclasses. This keeps the parent class maintainers in control, and avoid unexpected bugs.
Final variables are also declared by using the same final
keyword.
For example, in the following method, the argument id
and the local variable transaction
are final.
class Payment {
void executeTransaction(final Long id) {
final Transaction transaction = getCurrentTransaction();
// rest of the code omitted for brevity
}
}
Something to note here is that if we declare a variable without the final
keyword,
and it’s never modified again, then it’s
effectively final.
The use cases for final variables range from code standards and best practices established in some companies, to language requirements such as lambda expressions. Let’s analyze this last one.
Lambda expressions require that any local variable, formal parameter, or exception parameter used but not declared in a lambda expression must either be final or effectively final (§15.27.2).
Let’s take as an example the following code:
class Payment {
void executeTransaction(Long id) {
Transaction transaction = getTransaction();
queueTasks(() -> System.out.printf("Queued tasks for transaction %s and id %s", transaction, id));
}
Transaction getTransaction() { return new Transaction(); }
void queueTasks(Runnable callback) {
// code omitted for brevity
callback.run();
}
}
class Transaction {}
For the sake of simplicity, let’s focus only on the method executeTransaction
(and ignore the auxiliary methods).
void executeTransaction(Long id) {
Transaction transaction = getTransaction();
queueTasks(() -> System.out.printf("Queued tasks for transaction %s and id %s", transaction, id));
}
In its body, there’s a call to queueTasks
which accepts a Runnable
parameter.
Because
Runnable
is a functional interface
we can provide the argument as a lambda expression.
In the body of the lambda expression the variable transaction
and the parameter id
are used
without being declared final
explicitly.
This is possible because they are effectively final.
If we were to re-assign a new value to either transaction
or id
then they would stop being effectively final.
This would cause a compile-time error reported when either of them is first accessed inside the lambda body.
The message would say something like Variable used in lambda expression should be final or effectively final
.
To avoid such error, we can declare them as final
and, if we ever re-assign a new value to them, we get a
compile-time error on the line where the re-assignment happens.
Declaring final
variables is also useful in scenarios where there are many variables declared
(common in long methods from legacy code), and we want to make sure none of them is re-assigned a new value.
Now that we’ve explored what’s the final
keyword used for and how to apply it, it’s time for you to put that
knowledge to work on your daily job.
Look again that class design and ask yourself: Is this class supposed to be subclassed? If yes, then document it, if not, then make it final. Your future self will thank you. You can always come back and make the class non-final, but you can’t make a class final without possibly breaking other code that could have extended your class (§13.4.2.3).
Questions like this should pop out of your mind when you design and implement your solutions. It will help with the overall code quality and the quality of the final product, which means happier final users.
One final word of caution.
With great power comes great responsibility.
While final
is powerful, its overuse can lead to inflexible code.
We should use it judiciously, considering the trade-offs between immutability and flexibility
and striving to achieve the correct balance.