Definition
- Sealed requirement – a sealed class or interface needs an explicit, stable set of permitted subtypes identified by fully qualified names in the
permitsclause or inferred from the same source file. - Local class – declared inside a method body, its name is scoped to that method and cannot be referenced from a
permitsclause outside the method. - Anonymous class – has no name at all, so there is nothing to list as a permitted subtype or as the sealing type itself.
- Class-file attribute – the compiler records permitted subtypes in the
PermittedSubclassesattribute using resolvable binary names, which local and anonymous classes lack. - Lambda exclusion – a lambda cannot implement a sealed interface because the compiler cannot synthesize a named permitted subtype for it.
Think of National Bank of India issuing a sealed rulebook that says "only these three named account products may extend AccountProduct: Savings, Current, and FixedDeposit." For that rule to mean anything, each permitted product must have an official, registered name that auditors can look up. A local class is like a scribble a teller makes on a sticky note inside one counter session. An anonymous class is like an unnamed temporary helper with no nameplate at all. You cannot put a scribble or a nameless helper on an official permit list.
Understanding the Concepts
Why can a sealed type not be local or anonymous?
A sealed type uses the form public sealed interface AccountProduct permits SavingsProduct, CurrentProduct, FixedDepositProduct. The permits clause lists the exact subtypes allowed. The compiler needs every permitted subtype to be a top-level or nested named type that it can resolve, because it writes the permitted list into the class file as the PermittedSubclasses attribute.
package com.bank.model;
import java.math.BigDecimal;
// Valid: top-level sealed interface with named subtypes
public sealed interface AccountProduct
permits SavingsProduct, CurrentProduct, FixedDepositProduct {
BigDecimal monthlyCharge();
}
final class SavingsProduct implements AccountProduct {
@Override
public BigDecimal monthlyCharge() {
return new BigDecimal("150.00");
}
}
final class CurrentProduct implements AccountProduct {
@Override
public BigDecimal monthlyCharge() {
return new BigDecimal("300.00");
}
}
final class FixedDepositProduct implements AccountProduct {
@Override
public BigDecimal monthlyCharge() {
return BigDecimal.ZERO;
}
}
What happens if you try to seal a local class?
A local class exists only inside a method body. The compiler rejects the sealed modifier on it because the class cannot be referenced from a permits clause outside that method.
package com.bank.service;
public class InvalidLocalSealed {
public void process() {
// COMPILE ERROR: sealed not allowed on local class
// sealed class Temp { }
// This does not compile because:
// 1. Temp's name is scoped to this method
// 2. No permits clause can reference it externally
// 3. The PermittedSubclasses attribute needs resolvable names
}
}
What happens if you try to seal an anonymous class?
An anonymous class has no name at all. Since sealing is about naming a closed set of heirs, sealed is meaningless and disallowed on anonymous classes.
package com.bank.service;
import java.math.BigDecimal;
public class InvalidAnonymousSealed {
interface AccountProduct {
BigDecimal monthlyCharge();
}
public void process() {
// COMPILE ERROR: cannot apply sealed to anonymous class
// AccountProduct p = new sealed AccountProduct() {
// public BigDecimal monthlyCharge() {
// return new BigDecimal("200.00");
// }
// };
}
}
Can a lambda implement a sealed interface?
No. A lambda cannot implement a sealed interface because the compiler cannot synthesize a named permitted subtype for the lambda's generated class. A functional interface used by a lambda must be effectively open for the compiler to create the implementation.
package com.bank.service;
import java.math.BigDecimal;
public class LambdaSealedDemo {
// This sealed functional interface cannot be used with a lambda
sealed interface FeeCalculator permits StandardFee {
BigDecimal calculate(BigDecimal principal);
}
final static class StandardFee implements FeeCalculator {
@Override
public BigDecimal calculate(BigDecimal principal) {
return principal.multiply(new BigDecimal("0.02"));
}
}
public static void main(String[] args) {
// This DOES work: using the named permitted subclass
FeeCalculator calc = new StandardFee();
System.out.println(calc.calculate(new BigDecimal("100000")));
// This does NOT compile:
// FeeCalculator lambdaCalc = (p) -> p.multiply(new BigDecimal("0.02"));
// Lambda target type must not be a sealed interface
}
}
2000.00
How does the PermittedSubclasses attribute work?
The compiler records the permitted subtypes in a class-file attribute called PermittedSubclasses, listing them by fully qualified binary name. The JVM re-validates this at load time. Local and anonymous classes lack stable binary names that can be written into this attribute.
package com.bank.service;
import java.util.Arrays;
public class PermittedSubclassReflection {
sealed interface AccountProduct
permits SavingsProduct, CurrentProduct {}
final record SavingsProduct(String name) implements AccountProduct {}
final record CurrentProduct(String name) implements AccountProduct {}
public static void main(String[] args) {
// Reflection shows the permitted subtypes
Class<?>[] permitted = AccountProduct.class.getPermittedSubclasses();
System.out.println("isSealed: " + AccountProduct.class.isSealed());
Arrays.stream(permitted)
.forEach(c -> System.out.println("Permitted: " + c.getSimpleName()));
}
}
isSealed: true
Permitted: SavingsProduct
Permitted: CurrentProduct
Can a local or anonymous class extend a sealed type?
A local or anonymous class can extend or implement a sealed type only through a non-sealed branch. It cannot be a direct permitted subtype (those must be named), but it can extend a non-sealed permitted subtype, which reopens the hierarchy.
package com.bank.service;
import java.math.BigDecimal;
public class LocalExtendingNonSealed {
sealed interface AccountProduct
permits StandardProduct, OpenProduct {}
final record StandardProduct(BigDecimal fee) implements AccountProduct {}
non-sealed interface OpenProduct extends AccountProduct {}
public void process() {
// Local class extending the non-sealed branch: allowed
class PromoProduct implements OpenProduct {
public BigDecimal discount() {
return new BigDecimal("50.00");
}
}
PromoProduct promo = new PromoProduct();
System.out.println("Promo discount: " + promo.discount() + " INR");
// Anonymous class extending non-sealed: also allowed
OpenProduct anonProduct = new OpenProduct() {};
System.out.println("Anonymous product created");
}
public static void main(String[] args) {
new LocalExtendingNonSealed().process();
}
}
Promo discount: 50.00 INR
Anonymous product created
What are the design implications?
The restriction is actually the feature working correctly. If the language allowed sealing a local or anonymous class, the closed-set guarantee would be meaningless because those types cannot be named on a permits list. The restriction forces proper architecture: sealed hierarchies use named, top-level or nested types that auditors and the compiler can enumerate.
package com.bank.model;
import java.math.BigDecimal;
// Production pattern: sealed hierarchy with named types
public sealed interface PaymentEvent
permits PaymentEvent.Transfer, PaymentEvent.Deposit, PaymentEvent.Withdrawal {
record Transfer(String from, String to, BigDecimal amount) implements PaymentEvent {}
record Deposit(String account, BigDecimal amount) implements PaymentEvent {}
record Withdrawal(String account, BigDecimal amount) implements PaymentEvent {}
}
Pankaj, the architect, designs all sealed hierarchies at National Bank of India using named records as leaves, which gives both the closed-set guarantee and clean pattern-matching support.
What should a senior engineer know?
The key senior point is that sealing exists to provide a stable, provable closed hierarchy for exhaustive switch and compile-time verification. Local and anonymous types are ephemeral and scoped, so they cannot provide that guarantee. This is why sealed hierarchies should always be modeled with named types at the top or nested level, using records where possible for clean deconstruction in pattern matching.
Visual Overview
┌─────────────────────────────────────────────┐ │ Can a sealed type be local or anonymous? │ ├─────────────────────────────────────────────┤ │ │ │ sealed interface AccountProduct │ │ permits X, Y, Z │ │ │ │ Each X, Y, Z must be: │ │ ┌──────────────────────────┐ │ │ │ Named type with stable │ │ │ │ binary name resolvable │ │ │ │ at compile time and │ │ │ │ recorded in class file │ │ │ └──────────────────────────┘ │ │ │ │ Local class? │ │ ┌──────────────────────────┐ │ │ │ Name scoped to method │──→ REJECTED │ │ │ Cannot be in permits │ │ │ └──────────────────────────┘ │ │ │ │ Anonymous class? │ │ ┌──────────────────────────┐ │ │ │ No name at all │──→ REJECTED │ │ │ Nothing to list │ │ │ └──────────────────────────┘ │ │ │ │ Lambda? │ │ ┌──────────────────────────┐ │ │ │ Cannot synthesize named │──→ REJECTED │ │ │ permitted subtype │ │ │ └──────────────────────────┘ │ │ │ └─────────────────────────────────────────────┘
Key Takeaway
A sealed class or interface restricts which types may extend or implement it, and every permitted subtype must be a named type known at compile time, because the compiler records that closed set in the class file's PermittedSubclasses attribute. A local class and an anonymous class can never be sealed. A local class has a name that exists only inside one method, so it can never be referenced from a permits clause elsewhere, and an anonymous class has no name at all.
In a banking project at National Bank of India, Pankaj designs sealed interfaces like AccountProduct that permit exactly three named records: savings, current, and fixed deposit. That closed set matches the compliance requirement that an auditor must be able to enumerate every possible product type. Rajesh's savings account and Priya's current account both map to permitted records, and because the set is closed, an exhaustive switch computes monthly charges with no default branch.
When a junior engineer tried to introduce a promotional product as a local sealed class inside a method, the compiler rejected it, and that rejection is the feature working correctly. A lambda also cannot implement a sealed interface, for the same naming and closed-set reasons. The only way a local or anonymous class can participate in a sealed hierarchy is through a non-sealed branch, which reopens the hierarchy at that point.
The practical takeaway is that sealed hierarchies should always be modeled with named, top-level or nested types, using records where possible. This gives both the closed-set guarantee and clean pattern-matching support that a regulated banking system needs.
Remember one sentence: sealing requires named, compile-time-known heirs, so local classes (method-scoped names) and anonymous classes (no names) can never be sealed.
---