Search⌘ K
AI Features

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.

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:

  1. 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.

  2. Instance fields: Fields belonging to an object. A final instance 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.

  3. 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.

Java 25
public class AccountSettings {
// 1. Static Constant: Belongs to the class, never changes
static final double TAX_RATE = 0.05;
// 2. Final Instance Field: Must be set once per object
final long accountNumber;
public AccountSettings(long id) {
// We must initialize final instance fields in the constructor
this.accountNumber = id;
}
public void calculateTotal(double price) {
// 3. Final Local Variable: Cannot be reassigned inside this method
final double total = price + (price * TAX_RATE);
System.out.println("Account: " + accountNumber);
System.out.println("Total with Tax: " + total);
// The following line would cause a compile-time error:
// total = 0;
}
public static void main(String[] args) {
AccountSettings account = new AccountSettings(10155L);
account.calculateTotal(100.0);
}
}
  • Line 3: We declare TAX_RATE as static final, making it a shared constant across all instances.

  • Line 6: We declare accountNumber as final. It effectively acts as a “read-only” ID for the object.

  • Lines 8–11: We initialize accountNumber in the constructor. If we skipped this, the compiler would report an error.

  • Line 15: We calculate total and mark it final.

  • Line 21: We note that reassigning total is 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).

Java 25
import java.util.ArrayList;
import java.util.List;
public class ReferenceExamples {
public static void main(String[] args) {
// The reference 'items' is final
final List<String> items = new ArrayList<>();
// Allowed: We can modify the object's internal state
items.add("Apple");
items.add("Banana");
System.out.println("Items: " + items);
// Not Allowed: We cannot reassign the reference to a new object
// items = new ArrayList<>(); // Compile-time error
}
}
  • Line 7: We declare items as a final reference to an ArrayList.

  • 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 items point to a new 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.

Java 25
class PaymentProcessor {
// Subclasses can change how they notify users
public void sendNotification() {
System.out.println("Sending default email...");
}
// Subclasses MUST NOT change the validation logic
public final void validateTransaction() {
System.out.println("Running core security validation...");
}
}
class CreditCardProcessor extends PaymentProcessor {
@Override
public void sendNotification() {
System.out.println("Sending SMS notification...");
}
// The following method would cause a compile-time error:
// @Override
// public void validateTransaction() {
// System.out.println("Skipping security check...");
// }
}
public class FinalMethodExample {
public static void main(String[] args) {
CreditCardProcessor processor = new CreditCardProcessor();
processor.sendNotification(); // Uses overridden version
processor.validateTransaction(); // Uses parent's final version
}
}
  • Lines 8–10: We mark validateTransaction as final. This guarantees that every PaymentProcessor, regardless of its subclass, performs the exact same security check.

  • Lines 14–17: We override sendNotification in the subclass, which is allowed because the parent method was not final.

  • Lines 20–23: We show that attempting to override validateTransaction would 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.

Java 25
// This class cannot be extended
final class SecureConfiguration {
public void displayConfig() {
System.out.println("Loading secure settings...");
}
}
// The following inheritance would cause a compile-time error:
// class HackedConfiguration extends SecureConfiguration { }
public class FinalClassExample {
public static void main(String[] args) {
SecureConfiguration config = new SecureConfiguration();
config.displayConfig();
}
}
  • Line 2: We declare SecureConfiguration as final.

  • Line 10: We note that trying to extend SecureConfiguration results in a compilation error because the compiler forbids inheritance from a final class.

    • 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.