final, sealed, non-sealed
Three keywords that all restrict what can extend or override something — final locks a hierarchy shut completely, while sealed and non-sealed (Java 17+) let you name exactly which classes are allowed to extend it.
Java gives you a spectrum of control over extensibility: open by default, final to forbid subclassing entirely, and — since Java 17 — sealed to permit subclassing only by an explicit, named list of classes. Together they let API designers say precisely how much freedom callers get.
final on a Class
A class declared final cannot be extended by any other class. This is useful when a class's correctness depends on its exact behavior never being overridden — String, Integer, and the other primitive wrapper classes are all final for exactly this reason: nothing can silently substitute an "almost-String" that violates String's immutability guarantee.
final class Currency {
private final String code;
Currency(String code) { this.code = code; }
String code() { return code; }
}
// class USD extends Currency { } // compile error: cannot inherit from final classfinal on a Method
A final method cannot be overridden by any subclass, even though the class itself may remain extensible. This locks down one piece of behavior — often because a superclass's other methods depend on that method's exact contract (a classic case is a method called from a constructor, where allowing overrides is genuinely dangerous).
A private method is implicitly non-overridable already (subclasses cannot even see it), so marking a private method final is redundant, though harmless.
class Account {
final double balance() { return computeBalance(); }
private double computeBalance() { return 0.0; }
}
class Savings extends Account {
// double balance() { return 999; } // compile error: cannot override final method
}final on a Variable — Reassignment, Not Immutability
final on a local variable, field, or parameter means the variable itself cannot be reassigned after its initial assignment. It says nothing about whether the object the variable refers to is immutable. This is one of the most commonly misunderstood points in Java: final locks the reference, not the referenced object's internal state.
names can never point at a different List object, but the List it does point at is fully mutable.
final java.util.List<String> names = new java.util.ArrayList<>();
names.add("Ada"); // fine: mutating the object, not reassigning the reference
names.add("Grace");
// names = new java.util.ArrayList<>(); // compile error: cannot reassign a final variableWhy final Doesn't Imply Immutability
Genuine immutability requires the object's own design — no exposed mutator methods, defensive copying of any mutable fields it holds, and typically final fields set once in the constructor. A final reference to a mutable object provides none of that; it only guarantees that one specific variable name will always point at the same object.
Never claim "final makes an object immutable." final constrains a variable binding; immutability is a property of the object's class design (see the Immutability topic).
Sealed Classes: Motivation
final is all-or-nothing: either any class can extend a type, or none can. Sometimes you want something in between — a type that can be extended, but only by a specific, known, finite set of subclasses. That is exactly what a sealed hierarchy models: a closed set of variants, useful for representing things like "a Shape is exactly a Circle, a Square, or a Triangle — nothing else." Sealed classes were finalized in Java 17.
Sealed Class Syntax
A class or interface is declared sealed and lists its allowed direct subtypes with a permits clause. Every class named in permits must, in turn, declare itself as exactly one of final, sealed, or non-sealed — there is no "leave it open by omission."
sealed interface Shape permits Circle, Square, Triangle { }
final class Circle implements Shape {
final double radius;
Circle(double radius) { this.radius = radius; }
}
final class Square implements Shape {
final double side;
Square(double side) { this.side = side; }
}
final class Triangle implements Shape {
final double base, height;
Triangle(double base, double height) {
this.base = base;
this.height = height;
}
}The Three Allowed Fates of a permits Subclass
Each class listed in a sealed type's permits clause must be declared as one of the following, closing off any ambiguity about further extension:
final — the subclass itself cannot be extended further; the hierarchy is fully closed at that branch.
sealed — the subclass can be extended, but only by its own explicit permits list, continuing the controlled hierarchy one more level.
non-sealed — the subclass reopens extensibility completely; any class anywhere can extend it. This is an explicit opt-out, so readers of the code can immediately see, at a glance, exactly where a closed hierarchy becomes open again.
non-sealed in Practice
non-sealed is a genuine escape hatch — use it sparingly and deliberately, since it reintroduces the exact unpredictability that sealed hierarchies exist to prevent.
sealed interface Payment permits CardPayment, CashPayment, OtherPayment { }
final class CardPayment implements Payment { }
final class CashPayment implements Payment { }
// OtherPayment deliberately reopens extension for plugin-style payment types.
non-sealed class OtherPayment implements Payment { }
// Any class, anywhere, can now extend OtherPayment:
class CryptoPayment extends OtherPayment { }permits Can Be Omitted When Subclasses Are in the Same File
If every permitted subclass is declared in the same source file as the sealed type, the permits clause can be omitted — the compiler infers the permitted set from the file's contents. This is a convenience for small, self-contained hierarchies; a permits clause spanning multiple files must be written out explicitly.
Sealed Classes and Pattern Matching
Sealed hierarchies pair naturally with pattern matching in switch (Java 21+): because the compiler knows the complete, closed set of subtypes, it can verify that a switch over a sealed type covers every case, and flag a missing branch as a compile error rather than a runtime surprise. This exhaustiveness checking is the main payoff of sealing a hierarchy.
static double area(Shape shape) {
return switch (shape) {
case Circle c -> Math.PI * c.radius * c.radius;
case Square s -> s.side * s.side;
case Triangle t -> 0.5 * t.base * t.height;
// No default needed: the compiler knows these are all the cases.
};
}final vs sealed vs non-sealed vs Ordinary (Open) Classes
class Open { } // any class may extend it, unrestricted
final class Closed { } // no class may extend it
sealed class Ctrl permits A, B { } // only A and B may extend it directly
non-sealed class Reopened extends Ctrl { } // any class may extend ReopenedRules and Behavior Summary
A sealed class/interface must be in the same module (or, for an unnamed module, the same package) as all of its permitted direct subclasses — sealing is not meant to constrain code you cannot see and verify together.
An abstract sealed class can still declare non-final concrete methods; sealing controls who may extend the type, not whether the type itself is abstract or how its methods behave.
final on a class, sealed on a class, and non-sealed on a class are mutually exclusive modifiers for that declaration — a given class picks exactly one stance on extensibility (or is left open by declaring none of the three).
Best Practices
Use final for classes and methods whose exact behavior other code relies on, or that simply have no sensible reason to be extended (utility classes, value-like types). Reach for sealed when you are modeling a fixed, known set of variants — especially when you want exhaustiveness checking in switch. Use non-sealed sparingly and only where you genuinely intend third-party or plugin-style extension.
Complete Example
sealed interface Vehicle permits Car, Motorcycle, Truck { }
final class Car extends RecordLikeBase implements Vehicle {
Car(String plate) { super(plate); }
}
final class Motorcycle extends RecordLikeBase implements Vehicle {
Motorcycle(String plate) { super(plate); }
}
non-sealed class Truck extends RecordLikeBase implements Vehicle {
Truck(String plate) { super(plate); }
}
// Truck deliberately allows further extension, e.g. for specialized fleets:
final class TankerTruck extends Truck {
TankerTruck(String plate) { super(plate); }
}
abstract class RecordLikeBase {
final String plate;
RecordLikeBase(String plate) { this.plate = plate; }
}Key Takeaways
final on a class or method forbids any further extension or overriding, respectively.
final on a variable forbids reassignment only — it never makes the referenced object immutable.
sealed (Java 17+) lets a class or interface declare exactly which classes are allowed to extend it, via permits.
Every permitted subclass of a sealed type must itself be final, sealed, or non-sealed — extensibility must be explicit at every level.
non-sealed deliberately reopens a branch of an otherwise closed hierarchy to unrestricted extension.
Sealed hierarchies enable compiler-checked exhaustiveness in switch pattern matching, catching missing cases at compile time.
final List<String> tags = new ArrayList<>(); //... later, treated as if tags itself can never change contents: assert tags.isEmpty(); // wrong assumption — final doesn't stop mutation final List<String> tags = new ArrayList<>(); tags.add("java"); // perfectly legal: the object is mutable, only the reference is fixed final only prevents reassigning the variable tags to a different List object; it does not prevent calling mutating methods on the List it already points to.
sealed interface Shape permits Circle, Square { } class Circle implements Shape { } // compile error: Circle must declare final/sealed/non-sealed sealed interface Shape permits Circle, Square { } final class Circle implements Shape { } final class Square implements Shape { }
Every class permitted by a sealed type must explicitly declare itself final, sealed, or non-sealed; a plain, unmodified class declaration is not allowed as a permitted subtype.
sealed class Base permits Impl { } // Impl.java in a different package with no module relationship class Impl extends Base { }
// Keep Base and all permitted subclasses in the same package (or module) sealed class Base permits Impl { } non-sealed class Impl extends Base { }
A sealed type's permitted subclasses must reside in the same module, or the same package for the unnamed module — sealing cannot span arbitrary, unrelated code locations.
// "This class is final so its fields must be immutable" final class Wrapper { java.util.List<String> items = new java.util.ArrayList<>(); // still mutable! } final class Wrapper { private final java.util.List<String> items = java.util.List.copyOf(java.util.List.of()); // truly immutable requires unmodifiable/defensive design, not just 'final' on the class } final on the class only prevents subclassing; it says nothing about whether the class's own fields or their referenced objects are mutable.
What exactly does final prevent when applied to a class? To a method? To a variable?
On a class: prevents any subclassing. On a method: prevents any subclass from overriding it. On a variable: prevents that specific variable from being reassigned after its first assignment — it does not restrict mutation of the object the variable refers to.
Why is String declared final in the JDK?
So that no subclass can override String's methods to violate its immutability contract; code throughout the JDK and in every Java program relies on String instances never changing value, and that guarantee would break if an arbitrary subclass could redefine behavior.
What must every class listed in a sealed type's permits clause do?
It must itself be declared final, sealed, or non-sealed — there is no way to leave a permitted subclass's own extensibility unspecified.
What does non-sealed mean, and why would a designer use it?
It marks a permitted subclass of a sealed hierarchy as deliberately reopened to unrestricted extension by any class. Designers use it when one branch of an otherwise closed, controlled hierarchy needs to support third-party or plugin-style extension.
What compiler benefit does sealing a hierarchy unlock for switch statements/expressions (Java 21+ pattern matching)?
Exhaustiveness checking: since the compiler knows the complete, closed set of permitted subtypes, it can verify a switch over the sealed type covers every possible case and flag any missing branch as a compile-time error.
True or false: 'final Point p = new Point(1, 2);' guarantees that the Point object p refers to can never change its coordinates.
False. It guarantees only that p can never be reassigned to point at a different object. Whether Point's coordinates can change depends entirely on Point's own class design (e.g., whether it exposes setters).
Can a sealed interface's permitted subclasses live in a completely different package with no shared module?
No. Java requires a sealed type's permitted direct subclasses to be in the same module, or in the same package when using the unnamed module.
Write a sealed interface Result with two implementations, Success (holding a String) and Failure (holding a String error message), each declared final.
sealed interface Result permits Success, Failure { } final class Success implements Result { final String value; Success(String value) { this.value = value; } } final class Failure implements Result { final String error; Failure(String error) { this.error = error; } }
Both Success and Failure satisfy the sealed hierarchy's requirement to be final, sealed, or non-sealed; since both are final, Result now has exactly two possible concrete shapes, enabling exhaustive switch handling later.
Does this compile? Why or why not?
It does not compile. Base.id() is declared final, so Derived cannot override it — the compiler rejects Derived's id() as an illegal override of a final method.
Write a sealed class hierarchy Shape (permits Circle, Polygon) where Polygon is itself sealed and permits Triangle and Square, demonstrating a multi-level sealed hierarchy.
sealed abstract class Shape permits Circle, Polygon { } final class Circle extends Shape { } sealed abstract class Polygon extends Shape permits Triangle, Square { } final class Triangle extends Polygon { } final class Square extends Polygon { }
Shape's direct permitted subtypes are Circle and Polygon. Polygon, itself sealed, further restricts extension to Triangle and Square. The complete, compiler-known set of concrete shapes is Circle, Triangle, and Square.
Which line causes a compile error, and why?
The last line, "scores = new java.util.HashMap<>();", fails to compile. scores is declared final, so it can never be reassigned to a different Map object after its initial assignment — even though the Map it already refers to is freely mutable, as shown by the two preceding put() calls.
class Base {
final int id() { return 1; }
}
class Derived extends Base {
int id() { return 2; }
}final java.util.Map<String, Integer> scores = new java.util.HashMap<>();
scores.put("Alice", 90);
scores.put("Bob", 85);
scores = new java.util.HashMap<>();