← Back to Advanced Java Course | Chapter 4: Modern Java Features | Lesson 4 of 12

Java Sealed Classes

A sealed class lets you list exactly which classes may extend it, like a club with a closed member list. Nobody else can join.
Syntax
java
sealed interface Name permits SubA, SubB { }

final class SubA implements Name { }
final class SubB implements Name { }

Introduction to Sealed Classes

Sealed classes let you explicitly restrict which other classes are allowed to extend or implement them, rather than leaving your hierarchy open to being extended by any unrelated class anywhere in the codebase. This gives you precise, deliberate control over exactly how your inheritance hierarchy can grow.

Example: Introduction to Sealed Classes

java
// Define the class Main
public class Main {
	sealed interface Shape permits Circle, Square {}
	// Define the record Circle (an immutable data carrier)
	record Circle(double radius) implements Shape {}
	// Define the record Square (an immutable data carrier)
	record Square(double side) implements Shape {}
	// Program entry point: the JVM starts running here
	public static void main(String[] args) {
		// Create a new Circle object and store it in s
		Shape s = new Circle(3);
		// Print a line to the console
		System.out.println(s);
	}
}

Subclass Constraints

Every direct subclass of a sealed class must itself be declared as one of exactly three things: final (closing that branch off entirely), sealed (continuing to restrict further extension), or non-sealed (reopening that branch to unrestricted extension). This chain of declarations is enforced by the compiler, so the hierarchy's shape can never drift silently.

Example: Subclass Constraints

java
public class Main {
	sealed static class Vehicle permits Car, Truck {}
	static final class Car extends Vehicle {} // final -- closes this branch
	static non-sealed class Truck extends Vehicle {} // non-sealed -- reopens it
	public static void main(String[] args) {
		Vehicle v = new Car();
		System.out.println(v.getClass().getSimpleName());
	}
}

Sealed Classes and Pattern Matching

Sealed classes pair especially well with pattern matching in switch statements, because the compiler already knows the complete, closed list of permitted subclasses. That means it can verify your switch covers every possible case at compile time, without you needing to add a defensive default branch.

Example: Sealed Classes and Pattern Matching

java
public class Main {
	sealed interface Shape permits Circle, Square {}
	record Circle(double radius) implements Shape {}
	record Square(double side) implements Shape {}
	static double area(Shape s) {
		return switch (s) { // compiler verifies every case is covered, no default needed
			case Circle c -> Math.PI * c.radius() * c.radius();
			case Square sq -> sq.side() * sq.side();
		};
	}
	public static void main(String[] args) {
		System.out.println(area(new Square(4)));
	}
}

Reflection and Sealed Classes

You can use reflection to inspect a sealed class at runtime, such as checking whether a given Class is sealed, or dynamically listing out its permitted subclasses via getPermittedSubclasses(). This is mostly useful for frameworks and tooling that need to introspect a sealed hierarchy generically.

Example: Reflection and Sealed Classes

java
// Define the class Main
public class Main {
	sealed interface Shape permits Circle, Square {}
	// Define the record Circle (an immutable data carrier)
	record Circle(double radius) implements Shape {}
	// Define the record Square (an immutable data carrier)
	record Square(double side) implements Shape {}
	// Program entry point: the JVM starts running here
	public static void main(String[] args) {
		// Print a line to the console
		System.out.println(Shape.class.isSealed());
		for (Class<?> c : Shape.class.getPermittedSubclasses()) {
			// Print a line to the console
			System.out.println(c.getSimpleName());
		}
	}
}

Real-World Domain Modeling

Sealed classes are a natural fit for modeling domain hierarchies where you genuinely want to represent a fixed, closed set of states or variants, such as the possible outcomes of a payment (Success, Declined, Pending) rather than an open-ended list anyone could extend later.

Example: Real-World Domain Modeling

java
// Define the class Main
public class Main {
	sealed interface PaymentResult permits Success, Declined, Pending {}
	// Define the record Success (an immutable data carrier)
	record Success(String transactionId) implements PaymentResult {}
	// Define the record Declined (an immutable data carrier)
	record Declined(String reason) implements PaymentResult {}
	record Pending(String reference) implements PaymentResult {}
	// Program entry point: the JVM starts running here
	public static void main(String[] args) {
		// Create a new Declined object and store it in result
		PaymentResult result = new Declined("Insufficient funds");
		// Print a line to the console
		System.out.println(result);
	}
}
Related Topics
{# common_mistakes/chapter_summary/browser_support: on Hindi pages the view already swaps in the hi_ translation fields (or blanks these out if untranslated), so this renders correctly for both languages without a lang_code check here. #}
Common Mistakes
  1. Declaring a permitted subclass without final, sealed or non-sealed, which is a compile error.
  2. Extending a sealed class with a class that is not listed in permits, which the compiler rejects.
  3. Adding a default branch to a pattern-matching switch over a sealed type, which stops the compiler from telling you when a new subclass is not handled.

Login to run this code

C/C++/Java/PHP execution requires a free account. Your code is saved — you'll land right back in the editor after logging in.