Back to Blog
Java

Java Sealed vs Final: Key Differences

java sealed vs final: Understand the difference between sealed and final classes in Java, and learn when to use each for controlled inheritance and maintainable domain...

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 java sealed vs final 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 the method dispatch is 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 or the same package (if no module). 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, use non-sealed or no modifier at all.

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 compiler will warn if you add a new OrderAction subtype and forget to update the switch. 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 compiler uses the sealed information for static checks, but the generated bytecode is similar to a non-sealed class. Reflection can still inspect the permitted subclasses via Class.getPermittedSubclasses(), which can be useful for serialization or validation frameworks.

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

A class can 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 by any class. 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 already implicitly sealed 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 the permitted subtypes must be accessible. If a permitted subclass is in a different package, it must be public or protected, and the base type 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