The final Keyword
Explore how the final keyword in Java enforces immutability and restricts modification. Understand how final variables protect constant values, final methods ensure consistent behavior, and final classes prevent inheritance. This lesson helps you write safer, maintainable code by preventing accidental changes and bugs.
We'll cover the following...
Software development often prioritizes flexibility, but there are moments when we need to enforce restrictions to keep our code safe. Just as traffic rules prevent accidents by restricting where cars can go, we sometimes need to prevent other parts of our program from changing a value, redefining a method, or extending a class.
In Java, we use the final keyword to apply these restrictions. This allows us to communicate our design intent clearly: this specific piece of code is complete and should not be altered. By locking down certain behaviors, we prevent bugs caused by accidental modifications and build systems that are easier to reason about.
Final variables and constants
When we apply the final keyword to a variable, we are declaring that its value can be assigned exactly once. Once a final variable holds a value, it cannot be changed. This is useful for values that must remain constant throughout the execution of a method or the life of an object.
We can apply final in three common contexts:
Local variables: Variables inside a method. If we mark a local variable as
final, the compiler will generate an error if we try to reassign it.Instance fields: Fields belonging to an object. A
finalinstance field must be initialized either when it is declared or inside the class constructor. If we forget to initialize it, the code will not compile.Static constants: Variables that belong to the class itself and never change. We declare these as
static final. By convention, we name them using uppercase letters and underscores (e.g.,MAX_User_COUNT).
Using final ensures that values we expect to remain fixed, such as configuration settings or mathematical constants, are never accidentally overwritten.
Line 3: We declare
TAX_RATEasstatic final, making it a shared constant across all instances.Line 6: We declare
accountNumberasfinal. It effectively acts as a “read-only” ID for the object.Lines 8–11: We initialize
accountNumberin the constructor. If we skipped this, the compiler would report an error.Line 15: We calculate
totaland mark itfinal.Line 21: We note that reassigning
totalis illegal because it has already been assigned once.
Final references vs. final objects
A common source of confusion for beginners is how final interacts with reference types (objects). When we mark a variable holding an object as final, it means the reference is immutable, not the object contents.
We cannot point that variable to a different object, but we can change the data inside the object it points to.
Think of a final reference like a dog on a leash tied to a post. The dog (the object) cannot run to a different location (a new memory address), but it can still bark, sit, or eat (change its internal state).
Line 7: We declare
itemsas afinalreference to anArrayList.Lines 10–11: We call methods like
add()that modify the list’s internal data. This is perfectly legal.Line 15: Note that trying to make
itemspoint to anew ArrayList()would trigger a compiler error.
Final methods
Inheritance allows subclasses to override methods to provide specific behavior. However, sometimes we want to guarantee that a specific algorithm or check is never altered by a subclass. We use the final keyword on a method to prevent it from being overridden.
This is common in security checks or core business logic where consistency is more important than flexibility. If a subclass attempts to override a final method, the compiler will stop it.
Lines 8–10: We mark
validateTransactionasfinal. This guarantees that everyPaymentProcessor, regardless of its subclass, performs the exact same security check.Lines 14–17: We override
sendNotificationin the subclass, which is allowed because the parent method was notfinal.Lines 20–23: We show that attempting to override
validateTransactionwould result in an error, protecting the integrity of the validation logic.
Final classes
Finally, we can apply the final keyword to an entire class. A final class cannot be extended. No other class can inherit from it.
We make a class final when we want to create a completely closed implementation. This is often done for immutable classes, where we want to ensure that no one can create a subclass that adds mutable state or changes behavior. The standard Java String class is a famous example of a final class; no one can create a BetterString subclass that alters how text works in Java.
Line 2: We declare
SecureConfigurationasfinal.Line 10: We note that trying to extend
SecureConfigurationresults in a compilation error because the compiler forbids inheritance from afinalclass.You can uncomment this line and attempt to run the program to see the specific error message generated by the Java compiler.
Lines 13–14: We can instantiate and use the class normally; we just cannot inherit from it.
We have now explored the three distinct roles of the final keyword. Whether applied to variables to ensure constant values, methods to protect critical logic, or classes to close off inheritance, final helps us write code that is safer and easier to maintain. By restricting what can change, we reduce the complexity of our applications and prevent entire categories of bugs related to unintended modifications.