In Java, sealed classes are a feature introduced in Java 15 (as a preview and finalized in Java 17) that allows you to control inheritance by specifying which classes or interfaces can extend or implement a given class or interface. This makes your class hierarchy more predictable and easier to reason about.
Using sealed classes involves the following key concepts:
1. Declaration of a Sealed Class
A class can be declared as sealed, which means that only a specific set of classes (declared permits) can extend that class. Here’s the basic syntax:
public sealed class ParentClass permits ChildA, ChildB {
// Class code
}
Here, only ChildA and ChildB (declared in permits) are allowed to extend ParentClass. This ensures complete control over the inheritance structure of your class.
2. The Role of Permitted Subclasses
Each subclass specified in the permits clause must do one of the following to complete the sealed hierarchy:
- Declare itself as
final(no further inheritance is allowed). - Declare itself as
sealed(allowing further controlled inheritance). - Declare itself as
non-sealed(allowing unrestricted inheritance).
Examples of each:
Final Subclass:
public final class ChildA extends ParentClass {
// Class code
}
Sealed Subclass:
public sealed class ChildB extends ParentClass permits GrandChild {
// Class code
}
public final class GrandChild extends ChildB {
// Class code
}
Non-Sealed Subclass:
public non-sealed class ChildC extends ParentClass {
// Class code
}
In the case of non-sealed, ChildC and its subclasses can be freely inherited, bypassing the restrictions of sealing.
3. Key Features and Benefits of Sealed Classes
- Ensure Complete Class Hierarchy Control:
- By listing all allowed subclasses, you can restrict who can build upon your functionality.
- Simplifies reasoning about the class hierarchy in complex systems.
- Improved Exhaustiveness Checking:
- When used with
instanceofor switch expressions, the compiler knows all the possible subclasses (because they’ve been explicitly listed). - For example, pattern matching with switch:
public String process(ParentClass obj) { return switch (obj) { case ChildA a -> "ChildA"; case ChildB b -> "ChildB"; default -> throw new IllegalStateException("Unexpected value: " + obj); }; } - When used with
- Enforces Encapsulation and API Design Consistency:
- Encourages developers to think hard about which subclasses make sense.
- Useful for Modeling Closed Systems:
- Great for scenarios where the possible subclasses represent a closed set of types, such as states in a state machine.
Example: Sealed Class for a Shape Hierarchy
Here is a practical example of using sealed classes in a geometric shape hierarchy:
public sealed class Shape permits Circle, Rectangle, Square {
// Common shape fields and methods
}
public final class Circle extends Shape {
// Circle-specific fields and methods
}
public final class Rectangle extends Shape {
// Rectangle-specific fields and methods
}
public final class Square extends Shape {
// Square-specific fields and methods
}
If someone tries to create a new subclass of Shape outside of those specified in permits, a compilation error will occur.
4. Rules and Restrictions
- A
sealedclass must use thepermitsclause unless all permitted implementations are within the same file. - The permitted classes must extend the sealed class or implement the sealed interface.
- Subclasses of sealed classes located in different packages must be
public. - All permitted classes are resolved at compile time.
Summary
Java’s sealed classes provide you with a powerful tool to control inheritance in your programs by explicitly defining the classes that are allowed to extend or implement a particular class or interface. They make your code more robust, predictable, and maintainable by restricting which subclasses can exist in a hierarchy. Use them when you want tight control over a class hierarchy or when modeling scenarios with a limited set of possibilities.
