Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Java  Module 6 – Inheritance, interfaces and polymorphism

Sealed classes and interfaces

Close a Java type hierarchy with sealed, permits, final and non-sealed, model payment methods as records, and let javac check every switch over them.

  • Intermediate
  • 25 minutes
  • Examples run with Java 25.0.4 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 25 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Limit a class hierarchy with sealed, permits, final and non-sealed
  • Model a closed domain such as payment methods with a sealed interface and records
  • Explain how sealing lets javac check that a switch handles every case
  • Read the compile errors that a new permitted type causes, and fix each one

Before you start

What you’ll learn · Before you start

What you will learn

  • Limit a class hierarchy with sealed, permits, final and non-sealed
  • Model a closed domain such as payment methods with a sealed interface and records
  • Explain how sealing lets javac check that a switch handles every case
  • Read the compile errors that a new permitted type causes, and fix each one

Before you start

On this page

A sealed class or interface names every class that may extend or implement it, in a permits clause, and both the compiler and the JVM refuse any other. Each permitted subclass then says how the hierarchy goes on: final stops it, sealed gives that branch a list of its own, and non-sealed opens the branch to anyone. Seal a type when it stands for a fixed set of alternatives, such as the ways a customer can pay. Because the compiler then knows every alternative, a switch that handles each one needs no default branch, and when someone adds an alternative, javac lists every switch that has to decide what to do with it. Sealed classes and interfaces are part of Java since Java 17 (JEP 409), and switches over them with type patterns since Java 21.

This lesson models a checkout’s payment methods as a sealed interface of records, breaks each sealing rule once to read what javac says, adds a fifth payment method to see which code the compiler stops and which code quietly charges the wrong fee, and compares sealed types with enums. Each session was recorded with Eclipse Temurin 25.0.4.1 (Java 25), and one file also runs on Java 17 to show what that release lacks. The fees are made up for the examples.

A closed set of payment methods

Payment permits four types. Three are records, which are implicitly final, so they qualify as permitted subtypes without another word. The fourth, Wallet, is an abstract class declared non-sealed: the checkout accepts wallets from partners, and each partner writes its own subclass, such as QuickPay, in its own code. The set of direct subtypes of Payment is closed; one branch below it stays open.

Sealed interface Payment permits three final records and one non-sealed Wallet class, which any partner may extend.sealed interface Paymentpermits exactly these fourrecord Cardfinal: no subclassesrecord Upifinal: no subclassesrecord CashOnDeliveryfinal: no subclassesnon-sealed class Walletopen again: anyone may extend itQuickPayany other partner's wallet

The sealed Payment hierarchy

Text description of the diagram

The diagram is one large box, "sealed interface Payment, permits exactly these four", with four boxes inside it, one under the other.

  1. "record Card", marked final: no class can extend it.
  2. "record Upi", also final.
  3. "record CashOnDelivery", also final.
  4. "non-sealed class Wallet", marked as open again: anyone may extend it. Inside it are two smaller boxes, "QuickPay" and "any other partner's wallet", the subclasses that partners write in their own code.

So the set of direct subtypes of Payment is closed and known to the compiler, while one branch of the hierarchy, Wallet, stays open for extension.

India-specific

UPI (Unified Payments Interface) is India’s system for instant transfers between bank accounts. A customer is identified by a virtual payment address such as asha@okbank, which is all that the Upi record holds.

A sealed interface, its records, a partner's wallet and the switches over them

checkout/Payment.java

/**
 * How a customer pays at checkout. Payment is sealed: the four types it permits are the only direct subtypes there
 * will ever be, so javac knows all of them.
 */
public sealed interface Payment permits Card, Upi, CashOnDelivery, Wallet {}

/** A record is implicitly final: nothing can extend it. */
record Card(String network, String last4) implements Payment {}

/** A UPI payment, identified by the customer's virtual payment address. */
record Upi(String vpa) implements Payment {}

record CashOnDelivery() implements Payment {}

/** Partner wallets: non-sealed opens this one branch again, so partners extend it in their own code. */
abstract non-sealed class Wallet implements Payment {
    abstract String provider();

    abstract long feePaise(long amountPaise);
}

checkout/QuickPay.java

/** A partner's wallet, written outside the checkout code: allowed, because Wallet is non-sealed. */
public class QuickPay extends Wallet {
    @Override
    String provider() {
        return "QuickPay";
    }

    /** 1 %, rounded down to whole paise. */
    @Override
    long feePaise(long amountPaise) {
        return amountPaise / 100;
    }
}

checkout/Fees.java

/** Fees and descriptions: one case per permitted type, and no default. */
public class Fees {

    /** The checkout's fee for a payment, in paise. */
    static long feePaise(Payment payment, long amountPaise) {
        return switch (payment) {
            case Card(String network, String last4) when network.equals("AMEX") -> amountPaise * 3 / 100;
            case Card card -> amountPaise * 2 / 100;
            case Upi upi -> 0;
            case CashOnDelivery cash -> 4_000;
            case Wallet wallet -> wallet.feePaise(amountPaise);
        };
    }

    static String describe(Payment payment) {
        return switch (payment) {
            case Card(String network, String last4) -> network + " card ending " + last4;
            case Upi(String vpa) -> "UPI to " + vpa;
            case CashOnDelivery() -> "cash on delivery";
            case Wallet wallet -> wallet.provider() + " wallet";
        };
    }
}

checkout/Main.java

import java.util.Arrays;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        long amount = 125_000; // 1,250.00
        List<Payment> payments = List.of(
                new Card("VISA", "4242"), new Card("AMEX", "0005"), new Upi("asha@okbank"), new CashOnDelivery(), new QuickPay());
        for (Payment p : payments) {
            System.out.println("%-22s fee %s".formatted(Fees.describe(p), money(Fees.feePaise(p, amount))));
        }
        System.out.println("Payment is sealed: " + Payment.class.isSealed());
        System.out.println("its permitted subtypes: " + Arrays.stream(Payment.class.getPermittedSubclasses()).map(Class::getSimpleName).toList());
        System.out.println("Wallet is sealed: " + Wallet.class.isSealed());
    }

    /** Paise as rupees with two decimals, the same in every locale. */
    static String money(long paise) {
        return "%d.%02d".formatted(paise / 100, paise % 100);
    }
}
Five payments, one fee each Python · checkout_session.py
from session import session, version

# Five payments through one sealed interface, with each fee and description chosen by a switch without a default.
version()
session(
    """
javac -d build checkout/Payment.java checkout/QuickPay.java checkout/Fees.java checkout/Main.java
java -cp build Main
"""
)

Output

# openjdk version "25.0.4.1" LTS
$ javac -d build checkout/Payment.java checkout/QuickPay.java checkout/Fees.java checkout/Main.java
$ java -cp build Main
VISA card ending 4242  fee 25.00
AMEX card ending 0005  fee 37.50
UPI to asha@okbank     fee 0.00
cash on delivery       fee 40.00
QuickPay wallet        fee 12.50
Payment is sealed: true
its permitted subtypes: [Card, Upi, CashOnDelivery, Wallet]
Wallet is sealed: false

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 checkout_session.py

Both methods of Fees are switches with one case for each permitted type and no default:

  • case Card(String network, String last4) when network.equals("AMEX") is a record pattern with a guard: it matches a Card, takes its two components apart, and applies only when the condition after when holds, so AMEX cards pay 3 % and every other card falls to case Card card at 2 %. The guarded case must come first. Written the other way round, the general case would match every card, and javac rejects the guarded one as “dominated by a preceding case label” (JEP 441).
  • case Wallet wallet covers QuickPay and every other subclass of Wallet, written by anyone, so the switch stays complete although that branch is open. Inside the open branch, each wallet computes its own fee: an ordinary overridden method.
  • The last three lines use reflection. Class.isSealed() and Class.getPermittedSubclasses() arrived with sealed classes in Java 17 and read the list that javac wrote into the class file.

The records and Wallet sit in Payment.java beside the interface. That keeps the closed set in one place, but the java launcher’s multi-file mode looks for each class in a file named after it (JEP 458), so it does not find classes declared in another class’s file. The session compiles the program with javac first; the exercise below nests its records inside the interface instead, which the launcher handles.

What javac checks in a sealed hierarchy

Sealing is a list, and javac holds both sides to it (JLS §8.1.1.2, §8.1.6, §9.1.4):

  1. Every permitted class must directly extend or implement the sealed type and declare final, sealed or non-sealed. Records are implicitly final, and enums implicitly final or sealed, so both fit as they are. An interface that extends a sealed interface must itself be sealed or non-sealed, since an interface cannot be final.
  2. No class outside the list may extend or implement the sealed type.
  3. The permitted classes live in the same module as the sealed type or, in a program without modules, in the same package.
  4. When every permitted type is declared in the same file as the sealed type, the permits clause can be left out: javac permits exactly those types.
  5. Anonymous classes, local classes and lambdas are refused, because they have no name a permits clause could list. For the same reason a sealed interface is never a functional interface (JLS §9.8), even with a single abstract method.

The JVM enforces the list too: a sealed type’s class file carries its permitted subclasses, and the JVM refuses to load a class that claims a sealed supertype without being on that list (JEP 409). Broken.java breaks rules 1, 2 and 5, and misuses non-sealed once; the files under shop break rule 3:

Six classes javac refuses, and a permits clause that crosses packages

rules/Broken.java

/** Six classes that a sealed hierarchy refuses, one per numbered comment. */
sealed interface Payment permits Card, Upi {}

record Card(String last4) implements Payment {}

// 1. A permitted class must say how the hierarchy goes on: final, sealed or non-sealed.
class Upi implements Payment {}

// 2. A class that the permits clause does not list.
final class Voucher implements Payment {}

// 3. non-sealed only reopens a sealed hierarchy; Plain has no sealed supertype.
non-sealed class Plain {}

/** A sealed interface with one abstract method. */
sealed interface Rule permits MaxAmount {
    boolean allows(long paise);
}

record MaxAmount(long limit) implements Rule {
    @Override
    public boolean allows(long paise) {
        return paise <= limit;
    }
}

class Checkout {
    void pay() {
        // 4. A lambda would be a new, unnamed class implementing Rule: a sealed interface is never functional.
        Rule anything = paise -> true;
        // 5. Nor may an anonymous class or (6.) a local class extend a sealed type.
        Payment gift = new Payment() {};
        record Coupon(String code) implements Payment {}
    }
}

shop/pay/Payment.java · rules/shop/pay/Payment.java

package shop.pay;

/** Permits a class of another package: refused, because the program is not in a named module. */
public sealed interface Payment permits Card, shop.partners.Voucher {}

shop/pay/Card.java · rules/shop/pay/Card.java

package shop.pay;

public record Card(String last4) implements Payment {}

shop/partners/Voucher.java · rules/shop/partners/Voucher.java

package shop.partners;

public record Voucher(String code) implements shop.pay.Payment {}
The rules of a sealed hierarchy, each broken once Python · rules_session.py
from session import session, version

# Broken.java breaks six rules of sealed hierarchies; then a sealed interface that permits a class of another
# package, in a program without modules.
version()
session(
    """
!javac -d build rules/Broken.java
!javac -d build rules/shop/pay/Payment.java rules/shop/pay/Card.java rules/shop/partners/Voucher.java
"""
)

Output

# openjdk version "25.0.4.1" LTS
$ javac -d build rules/Broken.java
rules/Broken.java:7: error: sealed, non-sealed or final modifiers expected
class Upi implements Payment {}
^
rules/Broken.java:10: error: class is not allowed to extend sealed class: Payment (as it is not listed in its 'permits' clause)
final class Voucher implements Payment {}
      ^
rules/Broken.java:13: error: non-sealed modifier not allowed here
non-sealed class Plain {}
           ^
  (class Plain does not have any sealed supertypes)
rules/Broken.java:30: error: incompatible types: Rule is not a functional interface
        Rule anything = paise -> true;
                        ^
rules/Broken.java:32: error: anonymous classes must not extend sealed classes
        Payment gift = new Payment() {};
                                     ^
rules/Broken.java:33: error: local classes must not extend sealed classes
        record Coupon(String code) implements Payment {}
        ^
6 errors
[exit status 1]
$ javac -d build rules/shop/pay/Payment.java rules/shop/pay/Card.java rules/shop/partners/Voucher.java
rules/shop/pay/Payment.java:4: error: class Payment in unnamed module cannot extend a sealed class in a different package
public sealed interface Payment permits Card, shop.partners.Voucher {}
                                                           ^
1 error
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 rules_session.py

The messages map onto the rules one by one. Upi names no modifier (“sealed, non-sealed or final modifiers expected”). Voucher is not on the list (“not listed in its ‘permits’ clause”). Plain uses non-sealed without a sealed supertype, which means nothing. The lambda, the anonymous class and the local record would each be a new class without a name. In the last run, shop.partners.Voucher is in another package, and the program has no module-info.java, so javac refuses the permits clause itself.

Adding a payment method

Sealing pays off when the set changes. A fifth method, NetBanking, needs one word in the permits clause and one new record, and the checkout’s fees must then cover it. The second tab is the old fee written two more ways, with a default branch and as an if-else chain:

NetBanking added, and two fee methods that javac cannot check

more/Payment.java · more/Payment.java

/**
 * How a customer pays at checkout. Payment is sealed: the four types it permits are the only direct subtypes there
 * will ever be, so javac knows all of them.
 */
public sealed interface Payment permits Card, Upi, CashOnDelivery, Wallet, NetBanking {}

/** A record is implicitly final: nothing can extend it. */
record Card(String network, String last4) implements Payment {}

/** A UPI payment, identified by the customer's virtual payment address. */
record Upi(String vpa) implements Payment {}

record CashOnDelivery() implements Payment {}

/** Partner wallets: non-sealed opens this one branch again, so partners extend it in their own code. */
abstract non-sealed class Wallet implements Payment {
    abstract String provider();

    abstract long feePaise(long amountPaise);
}

/** New: a payment through the customer's bank. */
record NetBanking(String bank) implements Payment {}

checkout/LenientFees.java

/** The same fee written two other ways, which javac cannot check. */
public class LenientFees {

    /** A switch with a default branch: every type that is not listed falls into it. */
    static long withDefault(Payment payment, long amountPaise) {
        return switch (payment) {
            case Card card -> amountPaise * 2 / 100;
            case Wallet wallet -> wallet.feePaise(amountPaise);
            case CashOnDelivery cash -> 4_000;
            default -> 0;
        };
    }

    /** An if-else chain: the last branch takes whatever is left. */
    static long withIfChain(Payment payment, long amountPaise) {
        if (payment instanceof Card) return amountPaise * 2 / 100;
        else if (payment instanceof Upi) return 0;
        else if (payment instanceof Wallet wallet) return wallet.feePaise(amountPaise);
        else return 4_000;
    }
}

more/Try.java · more/Try.java

/** What the two unchecked versions charge for the new payment type. */
public class Try {
    public static void main(String[] args) {
        Payment p = new NetBanking("Example Bank");
        System.out.println("NetBanking, switch with a default: fee " + money(LenientFees.withDefault(p, 125_000)));
        System.out.println("NetBanking, if-else chain:         fee " + money(LenientFees.withIfChain(p, 125_000)));
    }

    static String money(long paise) {
        return "%d.%02d".formatted(paise / 100, paise % 100);
    }
}
What javac reports, and what the unchecked code charges Python · netbanking_session.py
from session import session, version

# A fifth payment type, NetBanking: javac lists the switches in Fees.java that do not handle it, while the switch
# with a default and the if-else chain of LenientFees.java compile and charge it a fee nobody chose.
version()
session(
    """
!javac -d added more/Payment.java checkout/QuickPay.java checkout/Fees.java
javac -d lenient more/Payment.java checkout/QuickPay.java checkout/LenientFees.java more/Try.java
java -cp lenient Try
"""
)

Output

# openjdk version "25.0.4.1" LTS
$ javac -d added more/Payment.java checkout/QuickPay.java checkout/Fees.java
checkout/Fees.java:6: error: the switch expression does not cover all possible input values
        return switch (payment) {
               ^
checkout/Fees.java:16: error: the switch expression does not cover all possible input values
        return switch (payment) {
               ^
2 errors
[exit status 1]
$ javac -d lenient more/Payment.java checkout/QuickPay.java checkout/LenientFees.java more/Try.java
$ java -cp lenient Try
NetBanking, switch with a default: fee 0.00
NetBanking, if-else chain:         fee 40.00

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 netbanking_session.py

javac refuses Fees.java and names both switches, at lines 6 and 16: “the switch expression does not cover all possible input values”. Those two lines are exactly the decisions the change needs, a fee and a description for net banking, and nothing compiles until someone makes them. LenientFees.java compiles without a word. Its switch puts NetBanking into the default branch and charges 0.00; its if-else chain lets it fall through to the last branch and charges it 40.00, as if it were cash on delivery. Neither fee was chosen by anybody.

So in a switch over your own sealed type, leave the default out. When several types share a result, list them in one case with unnamed patterns, case Upi _, CashOnDelivery _ -> 0 (JEP 456, Java 22), which still breaks the build when a new type arrives. What a class file compiled before the change does when it meets the new type at run time is shown in the low-level design track’s Java lesson, with a payment’s states instead of its methods.

A sealed type, an enum or an open interface?

Enum or sealed interface?
Criterion EnumSealed records
Fixed The set of valuesThe set of types
Data The same fields for every constantEach record has its own components
Switch Checked, with no defaultChecked, with no default
Who adds Only the enum's own fileOnly the sealed type's owners
When to choose Named values of one shape: seat classes, card networksAlternatives with different data: payment methods, results, events

An enum is the simpler tool when every alternative has the same shape: VISA and AMEX are values of one type. Once the alternatives carry different data, a card number for one and a payment address for another, an enum ends up with fields that are empty for most of its constants, and a sealed interface of records fits better. An open interface, not sealed at all, is right when you cannot know the alternatives, such as plugins that other code adds; the price is that no switch over it can be checked, so each one needs a default. That is why Wallet is the one open branch of Payment.

Version note

Sealed classes were previews in Java 15 and 16 and became final in Java 17 (JEP 409). Switches with type patterns and record patterns became final in Java 21 (JEPs 441 and 440), and the unnamed pattern _ in Java 22 (JEP 456). So Java 17 compiles a sealed interface but not the switch that makes it useful, as this run shows.

Versions.java Java · Versions.java
/** A sealed interface and a switch with type patterns: the first compiles on Java 17, the second needs Java 21. */
public class Versions {
    sealed interface Delivery permits Standard, Pickup {}

    record Standard(int km) implements Delivery {}

    record Pickup(String store) implements Delivery {}

    static String label(Delivery d) {
        return switch (d) {
            case Standard s -> "standard, " + s.km() + " km";
            case Pickup p -> "pickup at " + p.store();
        };
    }

    public static void main(String[] args) {
        System.out.println(label(new Standard(12)));
        System.out.println(label(new Pickup("Andheri")));
    }
}
The same file on Java 17 and on Java 25 Python · versions_session.py
from session import session, version

# The same file on Java 17, which has sealed interfaces but not yet switches with type patterns, and on Java 25.
version("17")
session("!java Versions.java", jdk="17")
version()
session("java Versions.java")

Output

# openjdk version "17.0.20.1"
$ java Versions.java
Versions.java:11: error: patterns in switch statements are a preview feature and are disabled by default.
            case Standard s -> "standard, " + s.km() + " km";
                 ^
  (use --enable-preview to enable patterns in switch statements)
1 error
error: compilation failed
[exit status 1]
# openjdk version "25.0.4.1" LTS
$ java Versions.java
standard, 12 km
pickup at Andheri

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 versions_session.py

Your turn: delivery fees

A shop delivers in three ways, modelled in Delivery.java as records nested in a sealed interface without a permits clause. DeliveryFees.feePaise() should compute each fee, and it returns 0 for everything so far:

The starter code and its sample checks

delivery/Delivery.java

/**
 * How an order reaches the customer. The three records are nested in the sealed interface, so the permits clause
 * can be left out: javac permits exactly the types declared in this file that implement Delivery.
 */
public sealed interface Delivery {

    /** Delivered within two days; `km` is the distance from the store in whole kilometres. */
    record Standard(int km) implements Delivery {}

    /** Delivered within two hours, and at night if `night` is true. */
    record Express(int km, boolean night) implements Delivery {}

    /** Collected by the customer at a store. */
    record Pickup(String store) implements Delivery {}
}

delivery/DeliveryFees.java

public class DeliveryFees {

    /**
     * The delivery fee in paise:
     * - Standard: 30.00 for the first 5 km, then 5.00 for each further kilometre;
     * - Express: twice the standard fee for the same distance, plus 50.00 at night;
     * - Pickup: nothing.
     * TODO: write it as one switch over the sealed interface, with a case for each record and no default.
     */
    static long feePaise(Delivery delivery) {
        return 0;
    }
}

delivery/Checks.java

/** The sample checks of the exercise: run with java delivery/Checks.java. */
public class Checks {
    private static int passed, run;

    private static void check(String what, long got, long expected) {
        run++;
        if (got == expected) passed++;
        System.out.printf("%s %-26s got %6d, expected %6d%n", got == expected ? "ok  " : "FAIL", what, got, expected);
    }

    public static void main(String[] args) {
        check("standard, 3 km", DeliveryFees.feePaise(new Delivery.Standard(3)), 3_000);
        check("standard, 5 km", DeliveryFees.feePaise(new Delivery.Standard(5)), 3_000);
        check("standard, 12 km", DeliveryFees.feePaise(new Delivery.Standard(12)), 6_500);
        check("express by day, 3 km", DeliveryFees.feePaise(new Delivery.Express(3, false)), 6_000);
        check("express at night, 12 km", DeliveryFees.feePaise(new Delivery.Express(12, true)), 18_000);
        check("pickup", DeliveryFees.feePaise(new Delivery.Pickup("MG Road")), 0);
        System.out.println(passed + " of " + run + " checks passed");
    }
}
The sample checks against the starter code Python · exercise_session.py
from session import session, version

# The sample checks against the starter code, whose fee is still 0 for every delivery.
version()
session("java delivery/Checks.java")

Output

# openjdk version "25.0.4.1" LTS
$ java delivery/Checks.java
FAIL standard, 3 km             got      0, expected   3000
FAIL standard, 5 km             got      0, expected   3000
FAIL standard, 12 km            got      0, expected   6500
FAIL express by day, 3 km       got      0, expected   6000
FAIL express at night, 12 km    got      0, expected  18000
ok   pickup                     got      0, expected      0
1 of 6 checks passed

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 exercise_session.py

  1. Write feePaise() as one switch expression over Delivery with a case for each record and no default. A standard delivery costs 30.00 for the first 5 km and 5.00 for each further kilometre; express costs twice the standard fee for the same distance, plus 50.00 at night; pickup costs nothing. Record patterns such as case Delivery.Express(int km, boolean night) give you the components directly.
  2. Run java delivery/Checks.java until it prints 6 of 6 checks passed.
  3. Add a fourth record, Scheduled(int km, String slot), for deliveries in a chosen two-hour slot. Before you change the switch, compile again and read the error javac gives you. Then give scheduled deliveries the standard fee plus 20.00.

Interview questions

Fresher · What is a sealed class in Java, and what must the classes it permits declare?

A sealed class or interface lists, in a permits clause, the only classes allowed to extend or implement it directly; the compiler refuses any other, and the JVM refuses to load one. Each permitted subclass must declare how the hierarchy continues: final, so nothing extends it; sealed, with a permits list of its own; or non-sealed, which opens that branch to any subclass. Records are implicitly final and enums implicitly final or sealed, so both can be permitted as they are. The permitted classes must be in the same module, or in the same package when there are no modules. The main benefit is that the compiler knows every subtype, so a switch over a sealed type that handles each one needs no default branch and fails to compile when a new subtype is added. Sealed classes are a standard feature since Java 17.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    Partners add export formats to your reporting product through plugins, and the core code switches over the format type in five places. A teammate proposes making Format a sealed interface so that the compiler checks those switches. When is that right, when is it wrong, and what would you design instead?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    Your library's public API has sealed interface Event permits Created, Paid, Cancelled, and client teams switch over it without default branches. Product needs a new Refunded event. What happens to clients that recompile against the new version, to clients that do not, and how would you release the change?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • sealed with permits closes the set of direct subtypes; each permitted type must be final, sealed or non-sealed, and records and enums qualify as they are.
  • Permitted types live in the same module, or in the same package without modules, and may leave out permits when they share the sealed type’s file.
  • A switch over a sealed type with a case for every permitted type needs no default, and adding a type makes javac list each switch that must handle it.
  • A default branch or an if-else chain compiles silently after such a change and gives the new type an answer nobody chose.
  • non-sealed reopens one branch for extension while the top of the hierarchy stays closed.
  • Use an enum for values of one shape, a sealed interface of records for alternatives with different data, and an open interface where other code must add alternatives.

Check yourself

6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 6 sealed interface Payment permits Card, Upi {}. Which declaration of Upi compiles?

    Choose one answer.

    Show the answer to question 1

    Answer: record Upi(String vpa) implements Payment {}

    A permitted subtype must be final, sealed or non-sealed. A record is implicitly final, so it needs nothing more. The two classes name none of the three modifiers, and an interface that extends a sealed interface must be declared sealed or non-sealed.

  2. Question 2 of 6 Wallet is a non-sealed class permitted by Payment, and a partner writes QuickPay extends Wallet in its own code. The switch in Fees has case Wallet wallet but nothing for QuickPay. What happens?

    Choose one answer.

    Show the answer to question 2

    Answer: The switch still compiles and is complete, because case Wallet matches every subclass of Wallet

    Exhaustiveness is about the permitted types of Payment, and Wallet is one of them. A type pattern for Wallet matches any object whose class extends Wallet, however many partners write such classes.

  3. Question 3 of 6 NetBanking has just been added to the permits clause of Payment. What does this method do with a NetBanking?

    Read the code, then choose one answer.

    static long fee(Payment payment, long amountPaise) {
        return switch (payment) {
            case Card card -> amountPaise * 2 / 100;
            case Wallet wallet -> wallet.feePaise(amountPaise);
            case CashOnDelivery cash -> 4_000;
            default -> 0;
        };
    }
    Show the answer to question 3

    Answer: It compiles without a warning and returns 0, the default branch

    A default branch covers every type, including the ones added later, so javac has nothing to report. Without the default, the same switch would fail to compile and point at the decision the change needs.

  4. Question 4 of 6 sealed interface Rule permits MaxAmount { boolean allows(long paise); } has one abstract method. Why does Rule r = paise -> true; not compile?

    Choose one answer.

    Show the answer to question 4

    Answer: A lambda would be a new class without a name, which the permits clause cannot list, so a sealed interface is never functional

    The Java Language Specification defines a functional interface as one that is not sealed and has a single abstract method (§9.8). Anonymous and local classes are refused for the same reason.

  5. Question 5 of 6 In the recorded run, what did Java 17 refuse in Versions.java?

    Choose one answer.

    Show the answer to question 5

    Answer: The switch with type patterns, a preview feature in Java 17

    Sealed classes are final in Java 17, but switches with type patterns only became final in Java 21 (JEP 441), so Java 17 reports that patterns in switch statements are a preview feature.

  6. Question 6 of 6 A payment method can be a card (network and last four digits), a UPI address or cash. Which model fits best?

    Choose one answer.

    Show the answer to question 6

    Answer: A sealed interface with one record per method, each with its own components

    The alternatives carry different data, which records express exactly, and sealing keeps every switch over them checked. An enum suits alternatives of one shape, and an open interface would give up the checks.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.