The final keyword in Java is a small word with a big job: it tells the compiler that something cannot change after it's been set. The "something" can be a local variable, a method parameter, an instance field, a method, or even an entire class, and the rules shift a little for each one. This lesson walks through every flavor of final in Java, the exact rule it enforces, and the patterns where each one fits.
final means "can only be assigned once" or "cannot be changed", with the exact meaning depending on what it precedes.
| Applied to | What gets locked |
|---|---|
| Local variable | Cannot be reassigned after its first value is set |
| Method parameter | Cannot be reassigned inside the method body |
| Instance field | Must be assigned exactly once, then cannot be reassigned |
| Static field | Must be assigned exactly once, then cannot be reassigned |
| Reference variable | The reference cannot point to a different object (the object itself can still mutate) |
| Method | Cannot be overridden in a subclass |
| Class | Cannot be extended by any subclass |
Every other rule about final in this lesson is a specialization of one of these rows. Keep this table in mind as we go. The interesting questions are usually "what counts as 'assigned'?" and "what does 'cannot change' actually protect?", not "what does the word final mean?"
final Local VariablesA local variable declared final can be assigned exactly once. The assignment can happen at the declaration or later in the same scope, but reassignment is a compile error.
Here taxRate is set at the declaration and never touched again. The final makes that promise explicit. A reader can scan the method and trust that taxRate stays 0.07 from declaration to return.
Assignment at the declaration isn't required. A final local can be left without an initial value, as long as the compiler can prove every path assigns it exactly once before it's read.
Both branches of the if assign shippingFee, so by the time the println runs, the compiler is sure it's been set exactly once. That's enough.
A second assignment is what the compiler refuses to accept.
What's wrong with this code?
The compiler reports:
Fix: Drop the final if the value needs to change, or pick a different variable for the new value.
The original constant stays final. A separate, non-final variable tracks the changing value. Usually one of these two is the right intent.
final Method ParametersA method parameter is a local variable that happens to receive its initial value from the caller. Marking it final prevents the method body from reassigning that variable.
The final here protects against a specific bug: code inside the method "reusing" a parameter to hold a different value, then reading it later as if it still held the caller's input. With final, that reuse fails at compile time, so the parameter always means what the parameter name says.
final on a parameter is purely about the method body. It doesn't change anything for the caller. A caller passing subtotal = 50.0 doesn't see any difference.
In practice, Java programmers don't sprinkle final on every parameter. Most teams reserve it for cases where it actively helps readability or where another language feature requires it (like accessing the parameter from an anonymous class or lambda). The compiler is happy either way.
final Instance FieldsA final instance field must be assigned exactly once. "Once" means exactly once on any path from object creation to use. The assignment can happen in three places:
The compiler checks all three locations together. The rule it enforces is: by the time a constructor finishes, every final instance field must have been assigned exactly once.
All three fields are assigned in the constructor. After the constructor returns, none of them can ever be reassigned. A Product built with ("P-1001", "Wireless Mouse", 29.99) holds those exact values for its entire life.
The fields that don't get an initial value at their declaration line are called blank finals. productId, name, and price above are all blank finals, because their declaration line gives only the type, not a value. Blank finals are useful when the value depends on the constructor arguments, which is most of the time for real data.
A field that has an initial value at the declaration is also valid, just less flexible.
These fields could also have been declared static final (covered below). The point: the assignment at the declaration counts as the one allowed assignment, and the constructor must not assign them again.
What's wrong with this code?
The compiler reports:
Fix: Every constructor must assign every blank final field. Either give price a value at the declaration, or assign it in this constructor.
With multiple constructors, the rule applies to each one independently. Each path that builds an object must assign every blank final exactly once.
final on a Reference VariableThis is the part of final most easily misunderstood. When a final variable holds a reference to an object, the lock applies to the reference (the arrow), not to the object (the thing the arrow points to). The reference can't be made to point somewhere else. The object itself is still free to change its internal state, if its class allows mutation.
The final modifier locks the arrow from items to the ArrayList. The contents of the ArrayList are a completely separate question.
cart is final, but calling cart.add(...) works fine. The add calls mutate the ArrayList that cart points to. They don't change which list cart points to. The only thing final forbids is cart = somethingElse.
That distinction matters because final is sometimes expected to "freeze" the object. It doesn't. To make the object's state unchangeable too, use either an unmodifiable wrapper (like List.copyOf(...)) or an immutable class. The rule to carry forward:
`final` on a reference locks the variable, not the object.
What's wrong with this code?
The error is on the second-to-last line:
Fix: For a fresh empty list, clear the existing one instead of reassigning. The reference stays the same, but the contents become empty.
If a different list is actually needed (perhaps because the old one is shared with something else), then final was the wrong choice and should be removed.
final MethodsA final method cannot be overridden by any subclass. The keyword sits on the method declaration, like this:
A subclass can still inherit and call calculateTotal, but it can't replace it with its own version. Trying to override it produces a compile error.
The intent of final on a method is to say: "this behavior is part of the class's contract, and subclasses shouldn't change it." A common case is methods that perform security or correctness checks. If a subclass could override validateOrder to skip a step, the protection would be optional. final removes that option.
Without a subclass, the final doesn't do anything visible. The protection only matters once someone tries to extend OrderValidator.
final ClassesA final class cannot be extended. No subclass can inherit from it. The keyword sits between the access modifier and class.
Writing class CustomProductId extends ProductId { ... } fails to compile with cannot inherit from final ProductId.
final on a class is a stronger statement than final on individual methods. Marking each method final only prevents overriding. Marking the class final also prevents anyone from adding new fields or methods through subclassing, and it signals to readers: "this class is the complete picture; don't try to extend it."
A few core JDK classes are final for exactly this reason:
| Class | Why it's final |
|---|---|
String | Immutability and string-pool sharing depend on subclasses not being able to alter behavior |
Integer, Long, Double, etc. | Same reasoning, plus they back primitive autoboxing |
LocalDate, LocalTime, LocalDateTime | Modern date/time API relies on immutability |
UUID | Identity values that must not be subclassed |
The reason class MyString extends String is not legal is this: String is final.
static final ConstantsA common use of final in production Java is to define constants. A constant is a value that's the same for every object of the class and never changes for the life of the program. The idiomatic spelling is public static final.
Three things are happening on those declarations:
public makes the constant accessible from any class.static makes it belong to the class itself, so callers write TaxConfig.TAX_RATE, not new TaxConfig().TAX_RATE.final makes the value unchangeable.The naming convention for constants is UPPER_SNAKE_CASE. Words are uppercase, separated by underscores. This convention is universal across the Java ecosystem, and it makes constants visually distinct from regular variables. MAX_ITEMS_PER_ORDER in code is recognizable as a constant without further inspection.
Constants earn their place when the same value would otherwise be repeated across many files. Imagine that 0.07 for tax rate is hardcoded in ten different methods. When the tax rate changes (and it will), every 0.07 has to be hunted through the codebase with a risk of missing one. With TaxConfig.TAX_RATE, one line changes.
CheckoutService doesn't store its own copy of the tax rate. It reads TaxConfig.TAX_RATE directly. If tax rules change, only TaxConfig needs an update.
A static final field whose value is a primitive or String literal and is set at declaration is called a compile-time constant. The Java compiler inlines compile-time constants at the call site. That means after compilation, the bytecode of CheckoutService no longer references TaxConfig.TAX_RATE symbolically; it has 0.07 literally baked in.
TAX_RATE, MAX_ITEMS, and CURRENCY are compile-time constants because their values are literal expressions known at compile time. DYNAMIC is static final (assigned exactly once at class load), but its value comes from a method call, so it's only known at runtime. Compile-time constants enable a couple of small things, like being used in case labels of a switch statement. Runtime-final values cannot be used there.
Inlining is a performance win at call sites (no field lookup), but it has a sharp edge. If TaxConfig is in a separate library and only CheckoutService is recompiled after changing TAX_RATE from 0.07 to 0.08, the old value is still inlined in any caller that wasn't recompiled. The fix is to recompile everything that depends on the constant. For values that change often, use a method like getTaxRate() instead.
final SitA single diagram that places each form of final next to what it locks:
The shape of the rule is the same across the table: final removes one specific kind of change. What changes is what kind of thing is being changed.
Java often treats a variable as if it were final even without the keyword, as long as it's never reassigned after its first value. Such a variable is called effectively final. The compiler computes this property automatically.
"Effectively final" isn't written anywhere; the property only matters when another part of the language requires final or effectively final variables. Two features that ask for it:
final or effectively final.The term comes up in an error message that eventually appears: "local variables referenced from a lambda expression must be final or effectively final". The cure is usually to stop reassigning the variable, or to copy its value into a fresh final one right before the lambda.
final to Work: A Small E-Commerce ClassA slightly larger example uses several of the final forms together. It models a LineItem on an order: the product ID, name, unit price, and quantity, all set when the line is created and untouched after.
Three forms of final are at work:
TAX_RATE is a public static final constant, shared by every LineItem and visible to outside code as LineItem.TAX_RATE.productId, name, unitPrice, and quantity are blank final instance fields, assigned exactly once in the constructor and frozen after.final here, to allow subclassing later. To lock the design down completely, add final class LineItem.This pattern (final fields set in the constructor, no setters) is a starting point for immutable classes. Immutable design has more rules (defensive copying for mutable fields, no methods that mutate state, sometimes a final class). For now, the takeaway is that final fields are one ingredient, not the whole recipe.
finalA few patterns are easy to get wrong. Each one comes from misunderstanding exactly which thing final is locking.
final to Deep-Freeze a Listcart is final, yet cart.clear() empties the list without complaint. final doesn't make the list immutable. For an immutable list, use List.copyOf(otherList) or List.of(...), both of which return a list that throws UnsupportedOperationException on any mutation attempt.
The error is: variable productId might not have been initialized. Fix: assign productId in the constructor, in an initializer block, or at the declaration.
this.name is already set when the second assignment runs, and a final field can only be assigned once. Fix: decide which value is correct and assign it once. For two valid options depending on context, use two constructors that each assign once.
final ParameterThe parameter is final, so reassignment inside the body is forbidden. Fix: introduce a new local variable for the normalized value.
The original parameter stays a faithful copy of what the caller passed in, and adjusted carries the post-normalization value. The two roles are split cleanly.
static final Numeric Literals As Free To RecompileA public static final int MAX = 50; in one library, used by a separate library, gets inlined. Changing MAX to 100 and recompiling only the constant's library leaves the other library still seeing 50 until it's rebuilt. Fix: rebuild every dependent library, or, for values that change often, expose the value through a method like getMax() instead of a constant.
final in Real CodeA few rules of thumb that hold up well in practice:
final on a local variable or parameter is documentation: "this variable doesn't change for the rest of this method."final on a field is a contract: "this object's value for this field is set at construction and never moves."final on a reference field (a List, Map, custom object) locks only the reference. The object can still mutate unless its class disallows it.final on a method or class is a hard limit: "no subclass gets to change this." That has consequences for testing (mocking a final method through a basic subclass is impossible) and for extension (callers must use composition, not inheritance).UPPER_SNAKE_CASE names are almost always static final constants. The casing alone signals the intent.final is a small word that makes constraints visible. The compiler enforces them, and readers benefit from the labels.
7 quizzes