Sealed classes enforce closed hierarchies that the compiler can verify exhaustively. Combined with pattern matching, they replace fragile instanceof chains with type-safe, readable code.
JOptimize Team
Before sealed classes, a method that returns a Shape could return any subclass — including ones the caller doesn't know about. The caller had to add an else branch for unknown types, and the compiler couldn't verify exhaustiveness. Sealed classes change this: the compiler knows exactly which subtypes exist and enforces that you handle all of them.
// Sealed interface — only these 3 implementations are permitted public sealed interface PaymentMethod permits CreditCard, BankTransfer, Cryptocurrency {} // Each permitted type is itself final (or sealed/non-sealed) public record CreditCard( String cardNumber, String cardholderName, YearMonth expiry, String cvv ) implements PaymentMethod {} public record BankTransfer( String iban, String bankName, String accountHolder ) implements PaymentMethod {} public record Cryptocurrency( String walletAddress, String network, // "ETH", "BTC", "SOL" BigDecimal fee ) implements PaymentMethod {}
Now PaymentMethod is a closed set — the compiler knows exactly 3 implementations exist.
public BigDecimal calculateFee(PaymentMethod method, BigDecimal amount) { return switch (method) { case CreditCard cc -> amount.multiply(new BigDecimal("0.025")); // 2.5% case BankTransfer bt -> new BigDecimal("0.50"); // Flat fee case Cryptocurrency c -> c.fee(); // Custom fee per crypto // No default needed — compiler verifies ALL cases are covered // Add a 4th PaymentMethod → compile error here immediately }; }
If you add PayPal to the sealed hierarchy, every switch over PaymentMethod without default produces a compile error. The compiler forces you to handle the new type.
// BEFORE Java 17 — verbose and fragile public String describePayment(PaymentMethod method) { if (method instanceof CreditCard) { CreditCard cc = (CreditCard) method; return "Card ending in " + cc.cardNumber().substring(cc.cardNumber().length() - 4); } else if (method instanceof BankTransfer) { BankTransfer bt = (BankTransfer) method; return "Bank transfer from " + bt.bankName(); } else if (method instanceof Cryptocurrency) { Cryptocurrency crypto = (Cryptocurrency) method; return "Crypto on " + crypto.network(); } else { throw new IllegalArgumentException("Unknown payment type"); } } // AFTER Java 21 — concise, type-safe, exhaustive public String describePayment(PaymentMethod method) { return switch (method) { case CreditCard cc -> "Card ending in " + last4(cc.cardNumber()); case BankTransfer bt -> "Bank transfer from " + bt.bankName(); case Cryptocurrency c -> "Crypto on " + c.network(); }; }
public String classifyPayment(PaymentMethod method, BigDecimal amount) { return switch (method) { case CreditCard cc when amount.compareTo(new BigDecimal("10000")) > 0 -> "High-value card payment — requires 3DS"; case CreditCard cc -> "Standard card payment"; case BankTransfer bt when bt.iban().startsWith("GB") -> "UK bank transfer — faster payments eligible"; case BankTransfer bt -> "International bank transfer"; case Cryptocurrency c -> "Crypto payment on " + c.network(); }; }
Guards (when) add runtime conditions to pattern matching — more specific cases must come before more general ones.
// Replace Optional<T> + exceptions with explicit result types public sealed interface OrderResult permits OrderCreated, OrderRejected, OrderPending {} public record OrderCreated(Order order, String confirmationId) implements OrderResult {} public record OrderRejected(String reason, ErrorCode code) implements OrderResult {} public record OrderPending(String queuePosition) implements OrderResult {} // Service returns sealed type — no exceptions for business logic public OrderResult placeOrder(PlaceOrderRequest req) { if (!inventoryService.hasStock(req.items())) { return new OrderRejected("Insufficient stock", ErrorCode.OUT_OF_STOCK); } if (req.total().compareTo(customerCredit(req.customerId())) > 0) { return new OrderRejected("Credit limit exceeded", ErrorCode.CREDIT_LIMIT); } if (isHighDemandPeriod()) { return new OrderPending(queueService.enqueue(req)); } Order order = orderRepository.save(Order.from(req)); return new OrderCreated(order, generateConfirmationId()); } // Controller handles all cases exhaustively @PostMapping("/orders") public ResponseEntity<?> createOrder(@RequestBody PlaceOrderRequest req) { return switch (orderService.placeOrder(req)) { case OrderCreated oc -> ResponseEntity.status(201).body(oc); case OrderRejected or -> ResponseEntity.status(422).body(or); case OrderPending op -> ResponseEntity.status(202).body(op); }; }
No try/catch for business logic. No null returns. The compiler guarantees all cases are handled.
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") @JsonSubTypes({ @JsonSubTypes.Type(value = CreditCard.class, name = "CREDIT_CARD"), @JsonSubTypes.Type(value = BankTransfer.class, name = "BANK_TRANSFER"), @JsonSubTypes.Type(value = Cryptocurrency.class, name = "CRYPTO") }) public sealed interface PaymentMethod permits CreditCard, BankTransfer, Cryptocurrency {}
JSON payload:
{ "type": "CREDIT_CARD", "cardNumber": "4111111111111111", "expiry": "2028-12" }
Jackson deserializes to the correct concrete type automatically.
default to switch over sealed types — defeats the exhaustiveness check; the compiler will no longer warn when you add a new permitted typefinal (records are final by default) or themselves sealed; non-sealed permits further subclasses but loses exhaustivenessSealed classes define closed type hierarchies that the compiler can verify exhaustively. Combined with Java 21 pattern matching switch expressions, they eliminate instanceof chains, make illegal states unrepresentable, and force callers to handle every case. The domain result pattern — sealed interface Result permits Success, Failure, Pending — replaces exception-based control flow with explicit, type-safe outcomes.
JOptimize flags instanceof chains that could be replaced with sealed classes, unhandled exception paths in service methods, and fragile type casting patterns.
Write safer Java with sealed types — free code modernization scan.
Master Spring Boot, security, and Java performance with hands-on courses.
JOptimize finds N+1 queries, EAGER collections, and 70+ other issues in your Java codebase — in under 30 seconds.