Java Final Keyword: Usage and Behavior
java final keyword: Explains how the final keyword works in Java for variables, methods, and classes, with practical examples and decision criteria for when to use it.
The java final keyword is a non-access modifier that can be applied to variables, methods, and classes. Its meaning changes depending on the context. A final field cannot be reassigned; a final method cannot be overridden; a final class cannot be extended. These rules are enforced at compile time, so violations produce errors rather than runtime surprises. Understanding the exact boundaries of final is more useful than treating it as a generic "make it immutable" tool, because final has different implications for each use.
What Exactly Does final Do to a Variable?
When you mark a variable as final, you are binding the variable's reference or value permanently. After the first assignment, any attempt to reassign that variable causes a compile error. This applies to local variables, instance fields, and static fields.
public class Order { private final long orderId = 10L; public void changeId() { // orderId = 20L; // compilation error } public void localFinal() { final double taxRate = 0.2; // taxRate = 0.25; // compilation error System.out.println(taxRate); } }
The assignment does not have to be in the declaration. A blank final variable can be assigned exactly once later, but the compiler must be able to prove that the assignment happens exactly once on every path. For instance fields, that usually means assigning in every constructor.
public class Session { private final long sessionId; public Session(long id) { this.sessionId = id; // mandatory assignment } }
Leaving a final instance field unassigned in a constructor results in a compile error, not a runtime null value or default primitive. This is a useful guard: a final field guarantees that the object is created with that field filled in.
A final variable that holds an object still allows modification of that object's state. Finalality is about the reference, not the underlying object. For example:
final List<String> names = new ArrayList<>(); names.add("Alice"); // allowed // names = new ArrayList<>(); // not allowed
This distinction is central to the practical use of final. When you want a deeply immutable collection, final alone is not enough; you also need an unmodifiable collection (for example, using List.copyOf or Collections.unmodifiableList).
How final Applies to Static Fields and Constants
A common pattern is to combine final with static to define a compile-time constant. The conventional naming style is uppercase with underscores.
public class Config { public static final int MAX_RETRIES = 3; }
The Java compiler can inline such constants in the code that references them, because their values are known at compile time. This means that if you change the constant's value and recompile only the class that declares it, other classes that were compiled against the old value may still use the old literal unless they are also recompiled. In practice, a full rebuild is expected.
For a constant object, the combination static final gives a fixed reference but not necessarily an immutable object.
public static final List<String> DEFAULT_TAGS = List.of("java", "programming");
List.of returns an immutable list, so this works well. If you used a normal ArrayList, the static final list could still be modified, which introduces shared mutable state into your application.
Why final Methods Matter Beyond Style
Marking a method as final prevents subclasses from overriding it. That is straightforward, but consider why you would want this ability. A final method locks in behavior that is central to the class's contract. For example, a template method that enforces an algorithm's structure may call protected abstract steps, but the overall flow should not be changeable.
public abstract class DataImporter { public final void importData(String filePath) { byte[] data = readFile(filePath); process(data); cleanup(); } protected abstract void process(byte[] data); private byte[] readFile(String filePath) { // implementation return new byte[0]; } private void cleanup() { // implementation } }
Here, importData is final to ensure subclasses cannot override the overall import sequence. They can only customize the process step. This is a stricter form of abstraction that preserves invariants.
A final method also has a subtle effect on the compiler's ability to inline method calls. In some JVM implementations, a private or final method can be inlined more aggressively, because there is no possibility of a dynamic dispatch to an override. However, the modern JIT can often inline virtual methods after profiling, so the performance difference is generally small. The main benefit of final methods is semantic clarity and preventing accidental overrides, not raw speed.
The Meaning of final on a Class
A final class cannot be subclassed. Typical examples are utility classes that exist only to hold static methods, or classes whose invariants rely on complicated internal state.
public final class StringUtils { public static String normalize(String input) { return input.trim().toLowerCase(); } }
When a class is final, all of its methods are effectively final as well, since there is no subclass to override them. This simplifies reasoning about the class's behavior and prevents inheritance-related surprises.
There is no option to have a partially final class; it is all or nothing. If you only want to prevent certain methods from being overridden, use final methods instead of making the whole class final.
A common choice is between a final class and a sealed class (available since Java 17). Sealed classes restrict which classes can extend them, giving more control than an outright ban. Use final when you want no subclasses at all; use sealed when you want to allow a controlled set of subclasses.
Interactions with Other Features: Lambdas, Records, and Local Classes
Final has special implications for lambdas and anonymous classes. A local variable that is used inside a lambda or an anonymous class must be effectively final. That means it must not be reassigned after initialization, even if it is not formally declared with final.
public void compute() { int factor = 2; Runnable r = () -> System.out.println(factor); }
Here factor is effectively final. If you try to reassign it, the code will not compile.
Records, introduced in Java 16, are implicitly final. Their fields are private and final, and the record itself cannot be extended. This gives you a compact way to create immutable data carriers without writing the whole boilerplate.
public record Point(int x, int y) { }
You cannot make a record non-final, and you cannot add mutable fields to it. If you need any of that, a regular final class with private final fields is the way to go.
Runtime Cost and Maintainability Tradeoffs
There is no meaningful runtime cost to using final. The compiler and JVM handle it as a compile-time constraint, and the JIT can decide to optimize based on the finality information, but you should not design around micro-performance gains.
The larger benefit is in code maintenance. A final variable communicates that a value will not change, making the flow easier to follow. A final method or class limits the extension points of your API, which reduces the risk of misuse. For example, if you expose a class as non-final, anyone can subclass it and potentially break expected behavior. Marking critical classes or methods final forces consumers to use composition instead of inheritance when that is more appropriate.
On the other hand, overusing final can make your API too restrictive. If you distribution a library, a final class cannot be extended by users, even if they have legitimate need. API design decisions about finality should be intentional. In application code, you have the freedom to change it later, so the maintainability benefit is more local.
When final Can Give a Wrong Sense of Security
A common misconception is that final guarantees thread safety. That is not true. A final field has some special memory-visibility guarantees in the Java Memory Model, specifically related to safe publication. When an object is properly published after construction, final fields are visible to other threads without synchronization. This can be useful, but it does not exempt other fields from visibility problems.
Consider a class with a final list field. The reference is visibly final, but the list's internal state can be mutated by multiple threads unsafely. Finalality does not make the list thread-safe.
Similarly, a final object reference does not prevent other code from calling mutable methods on that object. If you want a truly immutable value, you need to ensure that the object's state cannot be changed. Often that means using immutable collections and value objects with only getters.
For primitives, a final field is essentially like a constant. For references, you must think about the object's own immutability. In many cases, combining final with immutable collections or record types gives you the intended safety.
Practical Guidance for Applying final
Use final where the meaning is clear: constants with static final, unmodifiable references, and methods that must not be overridden. For local variables, using final when you want to clarify that a variable will not be reassigned can help readability, but adding it everywhere can create noise. Modern IDEs and the effective-final rule for lambdas reduce the need to write final explicitly on local variables.
A pragmatic default in application code is:
- Use final for class constants and configuration values.
- Use final for constructor parameters that are stored in final fields.
- Use final methods sparingly, mainly to enforce invariants.
- Use final classes for utility classes or when you are designing an API that should not be extended.
Treat final as a design tool, not a default. If you mark every method and class as final, you lose the flexibility to extend your code later, and you may interfere with testing frameworks that rely on subclassing (for example, creating a mock of a class). Modern mocking libraries like Mockito use inline mocking for classes, but final classes and methods were historically not mockable without special configuration. That is a practical constraint to consider.
When you write a new class, decide first what its inheritance behavior should be. If you have no valid reason to allow subclassing, final is a safe choice. If you anticipate extension, leave the class non-final and use final only on specific methods that must remain fixed.
Common Mistakes and How to Avoid Them
A frequent error is trying to modify a final collection reference. The reference cannot be reassigned, but the collection can be cleared or added to. If you intended the collection contents to be fixed, you need an immutable collection.
Another mistake is using final on a field but still allowing its value to escape into mutable state. For example:
public class MutableHolder { public final int[] values = {1, 2, 3}; }
The array can be modified by anyone with a reference to it, because arrays are inherently mutable. A safer pattern is to return a copy or use an immutable list.
When working with records, remember that the fields are final, but the record can still contain mutable components. A record with a list field is not deeply immutable. To get deep immutability, ensure the components themselves are immutable.
Finally, do not rely on final to make an object fully immutable for concurrency. The memory model guarantees visibility only for the final reference itself, not for the entire object graph unless all fields are final and the object is immutable by construction.
A Worked Example: Immutable Configuration Object
Putting the pieces together, consider creating an immutable configuration object. Use final fields to ensure the reference cannot change, and use immutable collections for any aggregated data.
import java.util.Map; public final class AppConfig { private final String name; private final int maxThreads; private final Map<String, String> settings; public AppConfig(String name, int maxThreads, Map<String, String> settings) { this.name = name; this.maxThreads = maxThreads; this.settings = Map.copyOf(settings); // defensive copy } public String name() { return name; } public int maxThreads() { return maxThreads; } public Map<String, String> settings() { return settings; // immutable because Map.copyOf creates an unmodifiable map } }
The use of Map.copyOf ensures that the original map's changes are not reflected, and the stored map is immutable. The class is final, so no subclass can compromise its invariants. Fields are final, so they cannot be reassigned. This design provides strong immutability, though not absolute deep immutability if values were mutable objects.
In a multi-threaded environment, safely published AppConfig instances benefit from the memory visibility guarantees of final fields, making the fields visible to other threads after construction completes. This is a subtle but useful property that goes beyond what a non-final field would offer.
The java final keyword is a small modifier that carries meaningful semantics in every context. Applying it deliberately makes your code more predictable and your APIs clearer about their boundaries. Understanding the exact behavior, rather than treating final as a synonym for "immutable," lets you use it where it provides real value.