Back to Advanced Topics
Q23Question 23 of 41

How does sealing interact with generics and wildcards?

Key Concepts
  • A sealed type can declare type parameters, and the permits clause names classes, not parameterized types.
  • Sealing closes the set of subtype classes while generics parameterize the data each carries; they constrain different things.
  • Because generics are erased at runtime, the PermittedSubclasses attribute records raw class names, and the closed set is class-level.
  • An exhaustive switch over a wildcard-typed sealed generic still only needs to cover the permitted classes, not the type arguments.
  • Pattern-match generic sealed types with wildcard patterns like `BuyOrder<?>` because the type argument is erased at runtime.
  • Sealing plus generics is a clean way to model type-safe heterogeneous results like Result<T> with Success<T> and Failure<T>.

Definition

Sealing closes the set of subtype classes while generics parameterize the data each carries, so the closed set and exhaustive matching hold regardless of type arguments or wildcards.
  • Sealed generic type – a sealed interface or class with a type parameter, e.g., sealed interface Order permits BuyOrder, SellOrder.
  • Permits names classes – the permits clause lists class names, not parameterized types, because generics are erased.
  • Exhaustiveness by class – a switch is exhaustive when every permitted class is covered, regardless of type arguments.
  • Wildcard patterns – match with BuyOrder because the exact type argument is not recoverable at runtime.
  • Type-safe modeling – Result with Success and Failure gives exhaustive handling with typed payloads.

Think of National Bank of India having a sealed set of order tickets: BuyOrder and SellOrder. Each ticket is stamped with the instrument it trades: equity, bond, or currency. The sealed rule fixes that there are exactly two ticket shapes. The generic parameter is the instrument stamp. A wildcard means "any ticket regardless of instrument." The closed set of two shapes stays closed no matter which instrument stamp is on them.

Understanding the Concepts

How do you declare a generic sealed type?

A sealed interface can declare a type parameter. Each permitted subtype decides how to handle the parameter: keep it generic, bind it to a concrete type, or add bounds.

JAVA
package com.bank.model;

import java.math.BigDecimal;

public sealed interface Order<T extends Instrument>
    permits BuyOrder, SellOrder {}

public record BuyOrder<T extends Instrument>(
    T instrument, BigDecimal quantity, BigDecimal price
) implements Order<T> {}

public record SellOrder<T extends Instrument>(
    T instrument, BigDecimal quantity, BigDecimal price
) implements Order<T> {}

public interface Instrument {
    String ticker();
}

public record Equity(String ticker, String exchange) implements Instrument {}
public record Bond(String ticker, String issuer) implements Instrument {}

Why does the permits clause name classes, not types?

The permits clause lists class names like BuyOrder and SellOrder, not parameterized types like BuyOrder. This is because generics are erased at runtime. The PermittedSubclasses attribute in the class file stores raw class names. The closed set is defined at the class level, independent of type arguments.

JAVA
// permits names classes:
public sealed interface Order<T extends Instrument>
    permits BuyOrder, SellOrder {}

// NOT parameterized types:
// permits BuyOrder<Equity>, SellOrder<Bond>  ← invalid syntax

How does exhaustiveness work with generics?

An exhaustive switch over Order only needs to cover BuyOrder and SellOrder. Exhaustiveness is checked against the closed set of permitted classes, not against the possible type arguments.

JAVA
package com.bank.service;

import com.bank.model.*;
import java.math.BigDecimal;

public class OrderProcessor {
    public static <T extends Instrument> BigDecimal notional(Order<T> order) {
        return switch (order) {
            case BuyOrder<T> b  -> b.quantity().multiply(b.price());
            case SellOrder<T> s -> s.quantity().multiply(s.price());
            // No default: exhaustive over the two permitted classes
        };
    }

    public static void main(String[] args) {
        var equity = new Equity("RELIANCE", "NSE");
        var buy = new BuyOrder<>(equity,
            new BigDecimal("100"), new BigDecimal("2450.75"));

        System.out.println("Notional: " + notional(buy));
    }
}
TEXT
Notional: 245075.00

How do wildcards work with sealed generics?

Wildcards work exactly as they do for any generic type. You can accept Order to read any order regardless of instrument.

JAVA
package com.bank.service;

import com.bank.model.*;
import java.math.BigDecimal;

public class RiskEngine {
    // Accepts any order of any instrument
    public static BigDecimal grossValue(Order<? extends Instrument> order) {
        return switch (order) {
            case BuyOrder<?> b  -> b.quantity().multiply(b.price());
            case SellOrder<?> s -> s.quantity().multiply(s.price());
        };
    }
}

Note the BuyOrder pattern: you use a wildcard because the type argument is erased at runtime.

How do you model Result with sealed generics?

Sealing plus generics is a clean way to build type-safe heterogeneous results.

JAVA
package com.bank.model;

public sealed interface Result<T> permits Success, Failure {}

public record Success<T>(T value) implements Result<T> {}
public record Failure<T>(String error, int code) implements Result<T> {}
JAVA
package com.bank.service;

import com.bank.model.*;
import java.math.BigDecimal;

public class ResultDemo {
    public static Result<BigDecimal> getBalance(String accountId) {
        if (accountId == null || accountId.isBlank()) {
            return new Failure<>("Account ID required", 400);
        }
        return new Success<>(new BigDecimal("50000.00"));
    }

    public static void main(String[] args) {
        Result<BigDecimal> result = getBalance("RAJESH-001");

        String output = switch (result) {
            case Success<BigDecimal>(var balance)
                -> "Balance: " + balance;
            case Failure<BigDecimal>(var error, var code)
                -> "Error " + code + ": " + error;
        };

        System.out.println(output);
    }
}
TEXT
Balance: 50000.00

How does a subtype fix the type parameter?

A permitted subtype can bind the type parameter to a concrete type, creating a specialized variant.

JAVA
package com.bank.model;

import java.math.BigDecimal;

// General order interface
public sealed interface Order<T extends Instrument>
    permits BuyOrder, SellOrder, EquityMarketOrder {}

// Keeps generic
public record BuyOrder<T extends Instrument>(
    T instrument, BigDecimal quantity, BigDecimal price
) implements Order<T> {}

public record SellOrder<T extends Instrument>(
    T instrument, BigDecimal quantity, BigDecimal price
) implements Order<T> {}

// Fixes T to Equity
public record EquityMarketOrder(
    Equity instrument, BigDecimal quantity
) implements Order<Equity> {}

How does bounded type propagation work?

Bounded type parameters on the sealed type propagate to permitted subtypes, keeping the domain consistent.

JAVA
// The bound T extends Instrument flows to all subtypes
// BuyOrder<String> would not compile because String
// does not extend Instrument

What should a senior engineer know?

Sealing closes the family of classes; generics type the payload. Wildcards let you consume the whole family at the use site. The closed set and exhaustive switch hold regardless of type arguments. Variance choices still belong to the use site via wildcards; sealing does not change variance. Generic sealed hierarchies like Result and Order are powerful modeling tools because they combine type safety with compiler-checked exhaustiveness.

Visual Overview

DIAGRAM
  Sealed Generic Hierarchy:

  sealed Order<T extends Instrument>
       │
       ├── BuyOrder<T>       (keeps T generic)
       ├── SellOrder<T>      (keeps T generic)
       └── EquityMarketOrder (fixes T = Equity)

  Exhaustiveness:

  switch (order) {
    case BuyOrder<?>          → handled
    case SellOrder<?>         → handled
    case EquityMarketOrder    → handled
    // No default: all 3 classes covered
  }

  Type arguments erased at runtime:
  BuyOrder<Equity> → BuyOrder (raw class)
  BuyOrder<Bond>   → BuyOrder (same raw class)
  Exhaustiveness checks classes, not type args

Key Takeaway

Sealing and generics constrain different things and work together cleanly. Sealing closes the set of subtype classes. Generics parameterize the data each subtype carries. The permits clause names classes, not parameterized types, because generics are erased. The PermittedSubclasses attribute stores raw class names, so the closed set is at the class level.

The key interaction for pattern matching is that an exhaustive switch over a wildcard-typed sealed generic only needs to cover the permitted classes, not the possible type arguments. You match with BuyOrder because the type argument is erased at runtime. This gives compiler-checked handling of every order action while keeping the instrument data type-safe at the API boundary.

At National Bank of India, Rajesh modeled trading orders as a generic sealed interface Order with BuyOrder and SellOrder. The risk engine accepted Order to process any order, and the exhaustive switch covered both classes with no default. For the Result pattern, Success and Failure gave exhaustive handling with typed payloads.

Remember one sentence: sealing closes the family of classes, generics type the payload, and the exhaustive switch holds regardless of type arguments because the closed set is at the class level.

---

Share
Use ← → arrow keys to navigate