How does the "final" keyword in Java work? (I can still modify an object.)
Categories:
Understanding Java's 'final' Keyword: Why Can I Still Modify an Object?

Explore the true meaning of the 'final' keyword in Java, distinguishing between immutability of references and mutability of objects, and how it impacts object-oriented programming.
The final
keyword in Java is a fundamental concept often misunderstood by new and even experienced developers. Many assume that declaring a variable final
makes the object it refers to immutable. However, this is a common misconception. While final
indeed prevents reassignment of the reference, it does not inherently prevent modifications to the object's internal state if that object is mutable. This article will clarify the precise behavior of final
with primitive types, object references, and methods, and demonstrate why you can still modify an object even if its reference is final
.
Final Variables: Primitives vs. Object References
The behavior of the final
keyword differs significantly depending on whether it's applied to a primitive type or an object reference. Understanding this distinction is crucial for writing correct and predictable Java code.
When final
is applied to a primitive type (like int
, boolean
, double
), the value of that primitive variable becomes constant and cannot be changed after its initial assignment. Any attempt to reassign it will result in a compile-time error.
public class FinalPrimitive {
public static void main(String[] args) {
final int MAX_VALUE = 100;
System.out.println("Initial MAX_VALUE: " + MAX_VALUE);
// MAX_VALUE = 200; // Compile-time error: cannot assign a value to final variable MAX_VALUE
}
}
Demonstrating final
with a primitive type
However, when final
is applied to an object reference, it means that the reference itself cannot be changed to point to a different object. The reference variable will always point to the same object that it was initially assigned. It does not mean that the state of the object it points to cannot be modified. If the object itself is mutable (i.e., it has methods that allow its internal state to be changed), then those changes are perfectly valid, even if the reference to that object is final
.
import java.util.ArrayList;
import java.util.List;
public class FinalObjectReference {
public static void main(String[] args) {
final List<String> names = new ArrayList<>();
System.out.println("Initial list: " + names);
// This is allowed: modifying the object's state
names.add("Alice");
names.add("Bob");
System.out.println("List after adding elements: " + names);
// This is NOT allowed: reassigning the final reference
// names = new ArrayList<>(); // Compile-time error: cannot assign a value to final variable names
}
}
Demonstrating final
with an object reference
flowchart TD A[Declare final variable] --> B{Is it a primitive type?} B -- Yes --> C[Value is constant, cannot be changed] B -- No --> D[It's an object reference] D --> E[Reference cannot be reassigned to another object] E --> F{Is the referenced object mutable?} F -- Yes --> G[Object's internal state CAN be modified] F -- No --> H[Object's internal state CANNOT be modified (it's immutable)] G --> I["Example: final List<String> list = new ArrayList<>(); list.add(\"item\"); is OK."] E --> J["Example: final List<String> list; list = new LinkedList<>(); is NOT OK."]
Decision flow for final
keyword behavior
Achieving Immutability
If the goal is to create an object whose state cannot be changed after creation, simply declaring its reference final
is not enough. True immutability requires careful design of the class itself. An immutable class ensures that once an instance is created, its internal state can never be altered.
To make a class immutable, you typically need to follow these guidelines:
1. Declare all fields final
This ensures that the references to other objects or primitive values within the immutable object cannot be changed.
2. Declare all fields private
This prevents direct access and modification from outside the class.
3. Don't provide setter methods
There should be no methods that allow modification of the object's state after construction.
4. Make the class final
This prevents other classes from extending it and potentially overriding methods in a way that breaks immutability.
5. Handle mutable object fields carefully
If your class holds references to mutable objects (e.g., Date
, ArrayList
), you must perform a 'defensive copy' in the constructor and in any getter methods. This prevents external code from modifying the internal state of your immutable object through those mutable references.
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public final class ImmutablePerson {
private final String name;
private final int age;
private final List<String> hobbies;
public ImmutablePerson(String name, int age, List<String> hobbies) {
this.name = name;
this.age = age;
// Defensive copy for mutable list
this.hobbies = new ArrayList<>(hobbies);
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public List<String> getHobbies() {
// Return an unmodifiable view to prevent external modification
return Collections.unmodifiableList(hobbies);
}
public static void main(String[] args) {
List<String> initialHobbies = new ArrayList<>();
initialHobbies.add("Reading");
initialHobbies.add("Hiking");
final ImmutablePerson person = new ImmutablePerson("Jane Doe", 30, initialHobbies);
System.out.println("Person: " + person.getName() + ", " + person.getAge() + ", Hobbies: " + person.getHobbies());
// Attempt to modify the original list used to construct the object
initialHobbies.add("Swimming");
System.out.println("Original hobbies list after modification: " + initialHobbies);
System.out.println("Person's hobbies (should be unchanged): " + person.getHobbies());
// Attempt to modify the list returned by the getter
try {
person.getHobbies().add("Running");
} catch (UnsupportedOperationException e) {
System.out.println("Caught expected exception: Cannot modify unmodifiable list.");
}
// person.age = 31; // Compile-time error: cannot assign a value to final variable age
// person = new ImmutablePerson("John Doe", 25, new ArrayList<>()); // Compile-time error
}
}
Example of an immutable class in Java
final
fields only guarantee that the reference cannot change. For true immutability, you must also ensure that the objects those references point to are themselves immutable, or that you handle mutable references with defensive copying.Final Methods and Classes
Beyond variables, the final
keyword can also be applied to methods and classes, serving different but related purposes in terms of restricting modification or extension.
Final Methods
When a method is declared final
, it cannot be overridden by subclasses. This is useful for preventing unwanted behavior changes in subclasses, ensuring that a particular implementation remains consistent across the inheritance hierarchy. It's often used for methods that are critical to the class's invariant or security.
class Parent {
public final void importantMethod() {
System.out.println("This is a critical method that cannot be overridden.");
}
public void regularMethod() {
System.out.println("This method can be overridden.");
}
}
class Child extends Parent {
// public void importantMethod() { // Compile-time error: cannot override final method
// System.out.println("Trying to override.");
// }
@Override
public void regularMethod() {
System.out.println("Child's overridden regular method.");
}
public static void main(String[] args) {
Child c = new Child();
c.importantMethod();
c.regularMethod();
}
}
Using final
with methods
Final Classes
Declaring a class final
prevents it from being subclassed. This is a powerful mechanism to ensure that the class's behavior cannot be extended or modified through inheritance. It's commonly used for security reasons (e.g., String
, Integer
, System
classes) or when a class is designed to be a complete, self-contained unit that should not be altered.
final class SealedClass {
public void display() {
System.out.println("This is a sealed class.");
}
}
// class AttemptToExtend extends SealedClass { // Compile-time error: cannot inherit from final SealedClass
// // ...
// }
public class FinalClassExample {
public static void main(String[] args) {
SealedClass s = new SealedClass();
s.display();
}
}
Using final
with classes
final
keyword is a tool for control and predictability. For variables, it controls reference assignment. For methods, it controls overriding. For classes, it controls inheritance. It's essential to use it judiciously to achieve the desired level of immutability or behavior enforcement.