Back to Blog
Java

Java Sealed vs Final: Key Differences

Sealed and final classes both control inheritance in Java, but they solve different problems. See the syntax, behavior, and when to use each.

sealed classesfinal classesJava inheritanceJava 17type design
Diagram comparing sealed and final class inheritance in Java

When designing Java APIs, you often need to control how other code extends your types. The final keyword has been the standard way to prevent subclassing entirely. Java 17 introduced sealed classes, which allow a more flexible middle option: you can restrict inheritance to a known set of subclasses. This article compares sealed and final classes in practical terms, showing syntax, behavior, and the tradeoffs you should consider.

What final Prevents

The final keyword applied to a class stops any subclass from being declared. Once you write final class Payment, no other class can extend Payment. The compiler enforces this rule, so any attempt to subclass it produces a compile-time error.

public final class Payment { private final double amount; public Payment(double amount) { this.amount = amount; } public double getAmount() { return amount; } }

final is absolute. It is useful when you have a value object that should never change behavior, or when you want to avoid the complexity of inheritance in a security-sensitive context. It also enables certain compiler optimizations because method dispatch can be statically bound.

The downside is that you cannot model a closed hierarchy. If you want to allow exactly two or three subclasses, final forces you to make the base class non-final and then rely on package-private constructors or other conventions to limit extension. That approach is fragile and not enforced by the compiler.

What sealed Adds

Sealed classes and interfaces let you declare the complete set of permitted subtypes. The base type uses the sealed modifier and a permits clause that lists every direct subclass or implementation.

public sealed class Shape permits Circle, Rectangle, Triangle { // common shape code } public final class Circle extends Shape { // circle-specific code } public non-sealed class Rectangle extends Shape { // rectangle-specific code } public final class Triangle extends Shape { // triangle-specific code }

Every permitted subclass must be in the same module. In an unnamed module, this means every permitted subclass must be in the same package. Each subclass must itself be final, sealed, or non-sealed. A final subclass ends the hierarchy for that branch. A sealed subclass continues the restriction with its own permits list. A non-sealed subclass opens the hierarchy back up, allowing arbitrary subclasses.

Sealed interfaces work the same way:

public sealed interface Result permits Success, Failure { // common result behavior } public final class Success implements Result { // success state } public final class Failure implements Result { // failure state }

Sealed types give you exhaustiveness. When you use a switch expression over a sealed type, the compiler can check that you have handled every possible subtype. This is a major advantage over final because it enables safe, maintainable pattern matching.

Key Differences Between final and sealed

The following table summarizes the most direct differences:

Aspectfinalsealed
SubclassingNone allowedOnly listed subtypes allowed
Subtype declarationsNot possibleMust be in same module/package
Subtype modifiersN/AMust be final, sealed, or non-sealed
Exhaustive switchingNot supportedSupported with pattern matching
Use caseImmutable value objects, securityClosed domain models, algebraic data types
Java versionSince Java 1.0Since Java 17 (preview in 15/16)

Both final and sealed restrict inheritance, but sealed gives you a controlled, compiler-checked set of subtypes. final is the simpler tool when you need no subtypes at all.

Choosing Between final and sealed

Use final when the type is a leaf in every possible scenario. Examples include simple value objects like Money, utility classes, or types that must not be extended for security reasons. If you never expect a meaningful subtype, final is the right choice.

Use sealed when you have a closed set of variants that you know at compile time. This is common in domain modeling: an order status, a payment method, a UI event, or a network response. Sealed types let you write exhaustive switches and make illegal states unrepresentable.

A good rule of thumb: if you can enumerate all possible subtypes in the current module, sealed gives you more flexibility than final without losing control. If you cannot enumerate them, or if you want to allow extension by external code, leave the base type open or use a non-sealed branch within a sealed hierarchy.

Modeling a Domain with Sealed Types

Consider a simple order processing system. You have three kinds of order actions: CreateOrder, UpdateOrder, and CancelOrder. With sealed types, you can model this cleanly:

public sealed interface OrderAction permits CreateOrder, UpdateOrder, CancelOrder { long orderId(); } public record CreateOrder(long orderId, String customerName) implements OrderAction {} public record UpdateOrder(long orderId, String newCustomerName) implements OrderAction {} public record CancelOrder(long orderId, String reason) implements OrderAction {}

Now you can process actions with an exhaustive switch:

public void handle(OrderAction action) { switch (action) { case CreateOrder c -> create(c); case UpdateOrder u -> update(u); case CancelOrder c -> cancel(c); } }

The switch pattern labels in this example require a Java version with pattern matching for switch; the feature was previewed in Java 17 and is standard in Java 21 and later. If you add a new OrderAction subtype, the compiler will report that this switch is no longer exhaustive. With final classes, you would need a base interface and then manually ensure exhaustiveness, which is error-prone.

If you had used final for each action class, you would still need a common interface, but you could not restrict the set of implementations. Any class could implement OrderAction, breaking the closed model.

Runtime and Maintainability Considerations

Sealed types do not add significant runtime overhead; the extra checks happen at compile time. The JVM records the permitted subclasses in bytecode so reflection can inspect them via Class.getPermittedSubclasses(), but ordinary method dispatch is unchanged.

One practical concern is compatibility. Sealed classes were introduced in Java 17. If your codebase must run on older Java versions, you cannot use them. final works everywhere. If you are on Java 17 or later, sealed types are a safer way to express a closed hierarchy than relying on package-private constructors or documentation.

Another maintainability benefit is that sealed types make the intended design explicit. A developer reading the code immediately sees the allowed subtypes. This reduces the chance of accidental extension and makes the domain model self-documenting.

Common Misconceptions and Edge Cases

Can a class be both sealed and final? No. The sealed modifier requires at least one permitted subtype, and final forbids all subtypes. They are mutually exclusive. You cannot combine them.

A sealed class can have a non-sealed subtype. That subtype can be extended without the extension being listed in the parent's permits clause. This is useful when you want to close most of the hierarchy but leave one branch open. For example, you might have a sealed interface Payment with CreditCard and BankTransfer as final records, but a non-sealed CryptoPayment that external libraries can extend.

Sealed types work with records and enums. Records are implicitly final, so they can be permitted subtypes. Enums are similarly closed because their constants are the only instances. You can also use sealed interfaces with records to get concise, immutable data carriers.

Another edge case is the permits clause. If you omit it, the compiler looks for subclasses in the same file. This is convenient for small hierarchies but can be confusing for larger ones. Always list the permitted subtypes explicitly unless the hierarchy is trivial and defined in a single file.

Finally, remember that permitted subtypes must be accessible to the sealed type. If a permitted subtype is in a different package, it must be accessible from the base type, and both types must be in the same module. This constraint keeps the hierarchy closed across package boundaries.

Java Sealed vs Final: Practical Usage and Code Examples | RYUSLOG DEV