Definition
- Orthogonal modifiers –
sealedcontrols which classes may extend the type;abstractcontrols whether it can be instantiated directly. - Abstract sealed class – cannot be instantiated, may declare abstract methods, and its concrete behaviour must come from its permitted subclasses.
- Each subclass – must be
final,sealed, ornon-sealed, exactly as with any sealed type. - Common base – holds shared state, constructors, and concrete helper methods; subclasses override the abstract parts.
- Algebraic data type – an abstract sealed class with final record or final class leaves is the cleanest Java encoding of an ADT.
Imagine National Bank of India defines an abstract concept of "Account" that describes what every account must do: report its balance and apply monthly processing. But you can never walk in and open a plain "Account"; you must open a specific product. At the same time, head office seals the concept so only three named products may ever be an Account. "Account" is both a blueprint you cannot instantiate directly (abstract) and a closed family with a fixed membership list (sealed).
Understanding the Concepts
Can you combine sealed and abstract?
Yes. sealed and abstract answer two different questions. abstract says "can I create an instance directly?" (no). sealed says "which classes may extend this?" (only the permitted list). They compose freely.
package com.bank.model;
import java.math.BigDecimal;
public abstract sealed class Account
permits SavingsAccount, CurrentAccount, LoanAccount {
private final String id;
private final String holder;
protected BigDecimal balance;
protected Account(String id, String holder, BigDecimal balance) {
this.id = id;
this.holder = holder;
this.balance = balance;
}
public String id() { return id; }
public String holder() { return holder; }
public BigDecimal balance() { return balance; }
// Concrete method shared by all
public void credit(BigDecimal amount) {
this.balance = this.balance.add(amount);
}
// Abstract method: each product calculates differently
public abstract BigDecimal monthlyInterest();
}
How do the permitted subclasses look?
Each subclass overrides the abstract method and calls the protected constructor via super().
package com.bank.model;
import java.math.BigDecimal;
public final class SavingsAccount extends Account {
private static final BigDecimal RATE = new BigDecimal("0.04");
public SavingsAccount(String id, String holder, BigDecimal balance) {
super(id, holder, balance);
}
@Override
public BigDecimal monthlyInterest() {
return balance().multiply(RATE).divide(new BigDecimal("12"), 2,
java.math.RoundingMode.HALF_UP);
}
}
package com.bank.model;
import java.math.BigDecimal;
public final class CurrentAccount extends Account {
public CurrentAccount(String id, String holder, BigDecimal balance) {
super(id, holder, balance);
}
@Override
public BigDecimal monthlyInterest() {
return BigDecimal.ZERO; // current accounts earn no interest
}
}
package com.bank.model;
import java.math.BigDecimal;
public final class LoanAccount extends Account {
private static final BigDecimal RATE = new BigDecimal("0.085");
public LoanAccount(String id, String holder, BigDecimal balance) {
super(id, holder, balance);
}
@Override
public BigDecimal monthlyInterest() {
return balance().multiply(RATE).divide(new BigDecimal("12"), 2,
java.math.RoundingMode.HALF_UP);
}
}
What is the impact on inheritance?
Because the base is abstract, no code can do new Account(...). All instantiation is pushed to the leaves. Because the base is sealed, the set of subclasses is fixed and known to the compiler. This combination enables exhaustive handling.
package com.bank.service;
import com.bank.model.*;
import java.math.BigDecimal;
public class InterestService {
public BigDecimal applyRegulatoryCap(Account account) {
BigDecimal cap = new BigDecimal("5000.00");
// Exhaustive switch: no default needed
return switch (account) {
case SavingsAccount s -> s.monthlyInterest().min(cap);
case CurrentAccount c -> c.monthlyInterest(); // always zero
case LoanAccount l -> l.monthlyInterest();
};
}
public static void main(String[] args) {
InterestService service = new InterestService();
SavingsAccount priya = new SavingsAccount(
"SAV-1001", "Priya", new BigDecimal("2000000"));
System.out.println("Priya's capped interest: "
+ service.applyRegulatoryCap(priya) + " INR");
}
}
Priya's capped interest: 5000.00 INR
How do abstract methods and exhaustiveness interact?
You can implement varying behaviour two ways: by overriding abstract methods in each subclass (classic polymorphism) or by switching on the sealed type in a service. Both are valid. Overriding keeps behaviour with the data; switching centralises behaviour in one place and benefits from compiler-checked exhaustiveness.
package com.bank.service;
import com.bank.model.*;
import java.math.BigDecimal;
public class FeeService {
// Approach 1: override abstract method in each subclass
// account.monthlyInterest() dispatches polymorphically
// Approach 2: exhaustive switch centralises logic
public BigDecimal calculateFee(Account account) {
return switch (account) {
case SavingsAccount s -> new BigDecimal("150.00");
case CurrentAccount c -> new BigDecimal("300.00");
case LoanAccount l -> new BigDecimal("500.00");
};
}
}
Can the abstract base have constructors?
Yes. An abstract sealed class can have constructors that subclasses call via super(). This lets you enforce invariants on common fields even though the base cannot be instantiated alone.
package com.bank.model;
import java.math.BigDecimal;
public abstract sealed class Transaction
permits Deposit, Withdrawal {
private final String accountId;
private final BigDecimal amount;
protected Transaction(String accountId, BigDecimal amount) {
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException(
"Amount must be positive: " + amount);
}
this.accountId = accountId;
this.amount = amount;
}
public String accountId() { return accountId; }
public BigDecimal amount() { return amount; }
}
final class Deposit extends Transaction {
Deposit(String accountId, BigDecimal amount) {
super(accountId, amount); // invariant check in base
}
}
final class Withdrawal extends Transaction {
Withdrawal(String accountId, BigDecimal amount) {
super(accountId, amount);
}
}
What if the sealed class is not abstract?
A sealed class that is not abstract can be instantiated and extended by its permitted subclasses. This is less common but occasionally useful when the base itself is a valid value.
package com.bank.model;
import java.math.BigDecimal;
// Concrete sealed class: both instantiable and extensible by permitted types
public sealed class Node permits SummaryNode {
BigDecimal value;
Node(BigDecimal value) {
this.value = value;
}
}
final class SummaryNode extends Node {
SummaryNode(BigDecimal value) {
super(value);
}
}
What are common mistakes with sealed abstract classes?
The most common mistake is declaring a sealed abstract class but permitting a non-sealed subclass too early, which reopens the hierarchy and defeats the purpose of sealing. Another error is forgetting to make the sealed class abstract when it should not be instantiated directly, leading to confusing constructor access. A third mistake is not leveraging exhaustive switch expressions, missing the main benefit of sealing.
package com.bank.model;
import java.math.BigDecimal;
public class SealedAbstractMistakes {
// WRONG: non-sealed subclass reopens hierarchy prematurely
// sealed abstract class Account permits SavingsAccount, CurrentAccount {}
// non-sealed class CurrentAccount extends Account {}
// class UnauthorizedSubclass extends CurrentAccount {} // Allowed!
// CORRECT: keep all subclasses final or sealed
sealed abstract static class Account permits SavingsAccount, CurrentAccount {
abstract BigDecimal monthlyFee();
}
static final class SavingsAccount extends Account {
BigDecimal monthlyFee() { return new BigDecimal("0.00"); }
}
static final class CurrentAccount extends Account {
BigDecimal monthlyFee() { return new BigDecimal("500.00"); }
}
public static void main(String[] args) {
Account acc = new SavingsAccount();
// Exhaustive switch -- compiler verifies all cases
String desc = switch (acc) {
case SavingsAccount s -> "Savings: " + s.monthlyFee();
case CurrentAccount c -> "Current: " + c.monthlyFee();
};
System.out.println(desc);
}
}
Expected output:
Savings: 0.00
What should a senior engineer know?
An abstract sealed class combined with final record or final class leaves is the cleanest Java encoding of an algebraic data type. You get a non-instantiable, closed, polymorphic family that the compiler fully understands. This is ideal for modelling a bank's fixed set of account products or transaction states with strong guarantees. The abstract base enforces shared invariants via constructors and provides concrete helper methods, while sealing ensures the compiler can prove exhaustiveness.
Visual Overview
┌─────────────────────────────────────────────┐ │ abstract sealed class Account │ │ permits Savings, Current, Loan │ ├─────────────────────────────────────────────┤ │ │ │ abstract = cannot instantiate directly │ │ sealed = only permitted subtypes exist │ │ │ │ Account (abstract + sealed) │ │ ┌──────┬──────────┬────────┐ │ │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ │ Savings Current Loan │ │ │ (final) (final) (final) │ │ │ │ │ │ Instantiation: │ │ │ new Account(...) → COMPILE ERROR │ │ │ new Savings(...) → OK │ │ │ │ │ │ switch(account): │ │ │ case Savings → ... │ │ │ case Current → ... │ │ │ case Loan → ... │ │ │ // No default: exhaustive ✓ │ │ └─────────────────────────────────────────────┘
Key Takeaway
Yes, a sealed class can also be abstract, because the two modifiers control different things. abstract controls whether the class can be instantiated directly (no), and sealed controls which classes may extend it (only the permitted list). Since they govern different concerns, Java lets you combine them, and the abstract sealed class is the most idiomatic form.
The abstract base holds common fields, constructors, and concrete helper methods, and declares abstract methods for the parts that vary. Each permitted subclass overrides the abstract parts and calls the base constructor through super(). The base constructor can enforce invariants like ensuring amounts are positive, which applies to every concrete type.
The impact on inheritance is twofold. Because the base is abstract, nobody can create a plain base instance, so all instantiation is pushed to the concrete leaves. Because the base is sealed, the set of subclasses is fixed and known to the compiler, which enables exhaustive handling: a switch over the abstract sealed type covers every subclass with no default branch, and adding a new permitted type breaks the build until it is handled.
In a banking project at National Bank of India, Pankaj models the account base as abstract and sealed, permitting savings, current, and loan. The interest service uses an exhaustive switch to apply a regulatory cap centrally, with no default branch because the compiler knows the set is complete. The savings account earns four percent, current earns nothing, and loan charges eight-point-five percent, and each is verified exhaustively.
Remember one sentence: a sealed class can be abstract because sealed controls who extends it and abstract prevents direct instantiation, and the combination is the idiomatic way to define a closed family of concrete types.
---