Key Concepts
- Sealed Classes: A sealed class restricts which classes can extend it.
- Permits Clause: The
permits
clause specifies the classes that are allowed to extend the sealed class. - Final, Non-Sealed, and Sealed Subclasses:
- Final: The subclass cannot be extended further.
- Non-Sealed: The subclass can be extended by any class.
- Sealed: The subclass can only be extended by the classes specified in its
permits
clause.
Syntax
To declare a sealed class, use the sealed
keyword followed by the permits
clause:
public sealed class Shape permits Circle, Rectangle, Square {
// common methods
}
Use Cases
- Exhaustive Pattern Matching: Sealed classes enable exhaustive pattern matching, as the compiler knows all possible subclasses.
- API Design: When designing APIs, sealed classes can be used to control the extension points, ensuring that only specific classes can extend a base class.
- Domain Modeling: In domain-driven design, sealed classes can model a closed set of types, such as a fixed set of states or events.
Example Implementation
Let’s consider a simple example of a geometric shape hierarchy.
Step 1: Define the Sealed Class
public sealed class Shape permits Circle, Rectangle, Square {
// Common properties and methods for all shapes
public abstract double area();
}
Step 2: Define Permitted Subclasses
public final class Circle extends Shape {
private final double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
}
public final class Rectangle extends Shape {
private final double width;
private final double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double area() {
return width * height;
}
}
public final class Square extends Shape {
private final double side;
public Square(double side) {
this.side = side;
}
@Override
public double area() {
return side * side;
}
}
Step 3: Using Sealed Classes
public class ShapeDemo {
public static void main(String[] args) {
Shape circle = new Circle(5);
Shape rectangle = new Rectangle(4, 6);
Shape square = new Square(3);
System.out.println("Circle area: " + circle.area());
System.out.println("Rectangle area: " + rectangle.area());
System.out.println("Square area: " + square.area());
printShapeArea(circle);
printShapeArea(rectangle);
printShapeArea(square);
}
public static void printShapeArea(Shape shape) {
if (shape instanceof Circle c) {
System.out.println("Circle area: " + c.area());
} else if (shape instanceof Rectangle r) {
System.out.println("Rectangle area: " + r.area());
} else if (shape instanceof Square s) {
System.out.println("Square area: " + s.area());
} else {
throw new IllegalStateException("Unexpected shape: " + shape);
}
}
}
Advanced Use Case: Sealed Interfaces
Sealed classes can also be used with interfaces to restrict which classes can implement them.
Step 1: Define the Sealed Interface
public sealed interface Vehicle permits Car, Truck, Motorcycle {
void drive();
}
Step 2: Define Permitted Implementations
public final class Car implements Vehicle {
@Override
public void drive() {
System.out.println("Driving a car");
}
}
public final class Truck implements Vehicle {
@Override
public void drive() {
System.out.println("Driving a truck");
}
}
public final class Motorcycle implements Vehicle {
@Override
public void drive() {
System.out.println("Riding a motorcycle");
}
}
Step 3: Using Sealed Interfaces
public class VehicleDemo {
public static void main(String[] args) {
Vehicle car = new Car();
Vehicle truck = new Truck();
Vehicle motorcycle = new Motorcycle();
car.drive();
truck.drive();
motorcycle.drive();
printVehicleType(car);
printVehicleType(truck);
printVehicleType(motorcycle);
}
public static void printVehicleType(Vehicle vehicle) {
if (vehicle instanceof Car) {
System.out.println("This is a car.");
} else if (vehicle instanceof Truck) {
System.out.println("This is a truck.");
} else if (vehicle instanceof Motorcycle) {
System.out.println("This is a motorcycle.");
} else {
throw new IllegalStateException("Unexpected vehicle type: " + vehicle);
}
}
}
If a class that is not listed in the permits clause of a sealed class attempts to extend that sealed class, the Java compiler will produce a compilation error. This is because the sealed class explicitly restricts which classes are allowed to extend it, and any class not listed in the permits clause is not permitted to do so.
Attempt to Define a Non-Permitted Subclass
public class Triangle extends Shape {
private final double base;
private final double height;
public Triangle(double base, double height) {
this.base = base;
this.height = height;
}
@Override
public double area() {
return 0.5 * base * height;
}
}
Compilation Error
When you try to compile the above code, you will get a compilation error for the Triangle
class because it is not listed in the permits
clause of the Shape
class.
Compilation Error Message
error: class is not allowed to extend sealed class: Shape
public class Triangle extends Shape {
^
Benefits of Sealed Classes and Interfaces
- Enhanced Readability and Maintainability: By explicitly listing the permitted subclasses, sealed classes make the codebase easier to understand and maintain.
- Exhaustive Pattern Matching: The compiler can ensure that all possible subclasses are handled, reducing the risk of runtime errors.
- Controlled Extensibility: API designers can control which classes can extend a base class or implement an interface, preventing unintended usage.
- Improved Security: By restricting subclassing, sealed classes can help prevent certain types of security vulnerabilities.
- Enhanced Readability and Maintainability: By explicitly listing the permitted subclasses, sealed classes make the codebase easier to understand and maintain.
- Exhaustive Pattern Matching: The compiler can ensure that all possible subclasses are handled, reducing the risk of runtime errors.
- Controlled Extensibility: API designers can control which classes can extend a base class or implement an interface, preventing unintended usage.
- Improved Security: By restricting subclassing, sealed classes can help prevent certain types of security vulnerabilities.
Summary
Sealed classes and interfaces in Java 17 provide a powerful tool for controlling inheritance hierarchies. They offer several benefits, including improved readability, maintainability, and security. By using sealed classes, developers can ensure that only specific classes can extend a base class or implement an interface, making the code more predictable and easier to manage.
Sealed classes and interfaces in Java 17 provide a powerful tool for controlling inheritance hierarchies. They offer several benefits, including improved readability, maintainability, and security. By using sealed classes, developers can ensure that only specific classes can extend a base class or implement an interface, making the code more predictable and easier to manage.
Additional Example: Sealed Classes in a Financial Application
Let’s consider a more complex example in a financial application where we have different types of transactions.
Step 1: Define the Sealed Class
public sealed class Transaction permits Deposit, Withdrawal, Transfer {
private final double amount;
public Transaction(double amount) {
this.amount = amount;
}
public double getAmount() {
return amount;
}
public abstract void process();
}
Step 2: Define Permitted Subclasses
public final class Deposit extends Transaction {
public Deposit(double amount) {
super(amount);
}
@Override
public void process() {
System.out.println("Processing deposit of $" + getAmount());
}
}
public final class Withdrawal extends Transaction {
public Withdrawal(double amount) {
super(amount);
}
@Override
public void process() {
System.out.println("Processing withdrawal of $" + getAmount());
}
}
public final class Transfer extends Transaction {
private final String fromAccount;
private final String toAccount;
public Transfer(double amount, String fromAccount, String toAccount) {
super(amount);
this.fromAccount = fromAccount;
this.toAccount = toAccount;
}
@Override
public void process() {
System.out.println("Processing transfer of $" + getAmount() + " from " + fromAccount + " to " + toAccount);
}
}
Step 3: Using Sealed Classes
public class FinancialApp {
public static void main(String[] args) {
Transaction deposit = new Deposit(1000);
Transaction withdrawal = new Withdrawal(500);
Transaction transfer = new Transfer(200, "AccountA", "AccountB");
processTransaction(deposit);
processTransaction(withdrawal);
processTransaction(transfer);
}
public static void processTransaction(Transaction transaction) {
transaction.process();
}
}
Conclusion
Sealed classes and interfaces are a significant addition to Java 17, providing developers with more control over inheritance hierarchies. They are particularly useful in scenarios where you want to limit the extension points of a class or interface, ensuring that only specific subclasses or implementations are allowed. This feature enhances code readability, maintainability, and security, making it a valuable tool for modern Java development.
Post a Comment