You are here
Home > java > Core Java >

Sealed Class In Java

Sealed Class In JavaJava has transformed remarkably over the years, introducing new features and enhancements to make the language more robust and secure. One such feature introduced in Java 15 as a preview is the ‘sealed’ keyword for classes and interfaces. It was included in Java 17 as a final feature. Sealed classes and interfaces provide improved control over the inheritance hierarchy. In this article, we will explore sealed class in Java & sealed interfaces, including their syntax, rules, code examples, and related concepts.

In Java, the sealed keyword is used to control and restrict the inheritance hierarchy of classes and interfaces. It allows us to specify which classes or interfaces are allowed to extend or implement a particular class or interface.

We can declare classes and interfaces using the sealed keyword in Java. The sealed keyword is not considered as a modifier, but rather a way to control the inheritance relationships within a hierarchy. We can use it at the class or interface level, not for methods or fields.

Why do we need sealed class in Java?

Inheritance is a fundamental concept of Java as an object-oriented programming language. It focuses on creating a hierarchy of related classes by inheriting properties and behaviors of one class to another class. The sealed classes/interfaces offer us an option to restrict the inheritance process as per our requirement.

For example, let’s consider a scenario where we have a base class Account to represent different types of bank accounts. An Account can be of various account types like SavingsAccount, CheckingAccount, and FixedDepositAccount. However, we want to ensure that SavingsAccount and FixedDepositAccount types are the only ones that can be created as subclasses of Account. Here, we want to restrict CheckingAccount from being subclassed. In this case, Sealed classes can be one of the option to implement this restriction.

This is where sealed class in java come into picture. They allow us to control and specify which classes or interfaces are permitted to become subclasses or implementers.

Let’s check how sealed class in java can address this scenario with code:

public sealed class Account permits SavingsAccount, FixedDepositAccount { }

public final class SavingsAccount extends Account { }    // Allowed

public final class FixedDepositAccount extends Account { }   // Allowed

public final class CheckingAccount extends Account { }   // Not Allowed

In this example, the Account class is declared as a sealed class with permits SavingsAccount, and FixedDepositAccount, specifying that only these two classes can extend it. As a result, no other classes (like CheckingAccount) can extend Account beyond the ones specified in the permits clause. Each specific account type extends Account and can add its own methods and properties specific to that account type.

How to declare a Sealed Class or Interface?

We use the ‘sealed’ keyword to declare a class as sealed class in java along with the permits clause to specify which classes are allowed to extend it. Let’s explore the syntax:

public sealed class ClassName permits Subclass1, Subclass2, ... {
         // class members
}

sealed: Indicates that the class is sealed.
ClassName: The name of the sealed class or interface.
permits Subclass1, Subclass2, …: Specifies the subclasses (for classes) or implementers (for interfaces) that are allowed.

To declare a sealed interface in Java, we use the sealed keyword along with the permits clause to specify which classes or interfaces are allowed to implement/extend it. Here’s the syntax:

public sealed interface InterfaceName permits Class1, Interface1, ... { 
         // Interface methods and constants
}

sealed: Indicates that the interface is sealed.
InterfaceName : The name of the sealed interface.
permits Class1, Interface1, …: In the permits clause, we list the classes or interfaces that are allowed to implement/extend this sealed interface. These are the implementing classes or extending interfaces that must explicitly declare their intention to implement the Sealed Interface.

Alternative Approach of Declaring a Sealed Class in Java: Without Using ‘permits’ Clause

Alternatively, If we define permitted subclasses in the same file as the sealed class, then we can omit the ‘permits’ clause. For example, below code demonstrates the concept.

package com.example.sealed.shape;

public sealed class Shape
            // The permits clause has been omitted
            // as its permitted classes have been
            // defined in the same file.
{ }

final class Circle extends Shape{
    float radius;
}

non-sealed class Square extends Shape{
    float side;
}

sealed class Rectangle extends Shape{
    float length, width;
}

final class FilledRectangle extends Rectangle {
    int red, green, blue;
}

In the above example, we don’t need to use ‘permits’ clause.

Rules and Constraints on Permitted Subclasses

To effectively use sealed classes and interfaces in our Java code, we need to understand the rules and guidelines associated with them:

1) Permitted subclasses must directly extend the sealed class.

For example, if X is a sealed class which permits Y to extend it and Y is also a sealed class that permits Z.
Then Z can only extend Y, it can not directly extend X.

2) The subclasses must have exactly one of the following modifiers:

  • final: Cannot be extended further
  • sealed: Can only be extended by its permitted subclasses
  • non-sealed: Can be extended by unknown subclasses; Non-sealed classes and interfaces can be subclassed or implemented without restrictions, even if they are in the same package as a sealed class or interface. non-sealed also means, that we are ending the parent-child hierarchy here.

3) They must be in the same module as the sealed class (if the sealed class is in a named module) or in the same package (if the sealed class is in the unnamed module).

For example, in the following declaration of com.example.mammals.Animal, its permitted subclasses are all in different packages. This example will compile only if Animal and all of its permitted subclasses are in the same named module.

package com.example.mammals;

public sealed class Animal permits 
           com.example.mammals.pet.Dog,
           com.example.mammals.orphan.Monkey
           com.example.mammals.wild.Tiger { 
}

4) A permitted subclass must extend the parent sealed class. For example, if sealed class A permits a class B, then B has to extend A. Similarly, the implementing class must explicitly implement the parent sealed interface.

5) A sealed class should specify the classes that may extend it using permits. This is not required if the child classes are defined inside parent class as an inner class or subclasses are defined in the same file.

Example Of Sealed Class in Java

Let’s create a simple example to illustrate sealed classes. We’ll define a sealed class Shape and permit two subclasses, Circle and Triangle.

public sealed abstract class Shape permits Circle, Triangle {
   public abstract double area();
}
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 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;
   }
}

In this example, Shape is declared as a sealed abstract class with permits Circle, Triangle, meaning only these two classes can extend Shape.
Circle and Triangle are both final classes, indicating that they cannot be further subclassed.
Both Circle and Triangle override the area() method as per the abstract contract defined in Shape.

Sealed Interfaces Example-1

Sealed interface works similarly to sealed class in Java. Let’s create an example with a sealed interface called Drawable, which permits two implementing classes, Circle and Rectangle.

public sealed interface Drawable permits Circle, Rectangle {
   void draw();
}
public final class Circle implements Drawable {
   private final double radius;

   public Circle(double radius) {
      this.radius = radius;
   }

   @Override
   public void draw() {
      System.out.println("Drawing a circle with radius: " + radius);
   }
}
public final class Rectangle implements Drawable {
   private final double width;
   private final double height;

   public Rectangle(double width, double height) {
       this.width = width;
       this.height = height;
   }

   @Override
   public void draw() {
      System.out.println("Drawing a rectangle with width: " + width + " and height: " + height);
   }
}

In this example, Drawable is declared as a sealed interface with permits Circle, Rectangle, meaning only these two classes can implement Drawable.
Circle and Rectangle both implement the Drawable interface and provide their own implementations of the draw() method.

Sealed Interfaces Example-2

Let’s create another example of sealed interfaces in Java where an interface extends another interface:

public sealed interface Vehicle permits LandVehicle, SeaVehicle {
   void start();
}

non-sealed interface LandVehicle extends Vehicle {
       void drive();
}

non-sealed interface SeaVehicle extends Vehicle {
       void navigate();
}

class Car implements LandVehicle {
   
   @Override
   public void start() {
      System.out.println("Car starting...");
   }

   @Override
   public void drive() {
      System.out.println("Car is driving...");
   }
}

class Ship implements SeaVehicle {

   @Override
   public void start() {
      System.out.println("Ship starting...");
   }

   @Override
   public void navigate() {
      System.out.println("Ship is navigating...");
   }
}

In this example, Vehicle is a sealed interface that permits two sub-interfaces: LandVehicle and SeaVehicle. Both LandVehicle and SeaVehicle extend the Vehicle interface and provide additional methods specific to land and sea vehicles. Car implements the LandVehicle interface, which means it must provide implementations for both the start() and drive() methods. Ship implements the SeaVehicle interface, requiring it to implement the start() and navigate() methods.

Sealed With Record: Record Classes as Permitted Subclasses

We can name a record class in the permits clause of a sealed class or interface. Sealed class in Java works very well with records. Since record is implicitly final, so it can only implement a sealed interface & the sealed hierarchy is even more concise. You may check Record Classes for more information.

As record can not be sub-classed, therefore it can not use permits keyword. Thus, there is a single level hierarchy with records.

Let’s implement an example with record classes instead of ordinary classes:

package com.example.records.expressions;

sealed interface Expression permits PlusExpr, MinusExpr, TimesExpr {
       public int eval();
}

public record PlusExpr(Expression a, Expression b) implements Expression {
     public int eval() {
        return a.eval() + b.eval();
     }
}

public record MinusExpr(Expression a, Expression b) implements Expression {
     public int eval() {
        return a.eval() - b.eval(); 
     }
}

public record TimesExpr(Expression a, Expression  b) implements Expression {
     public int eval() {
        return a.eval() * b.eval(); 
     }
}

In this example, record classes are implementing the sealed interface Expression. Since records are final by default, we don’t need to provide any modifier before the implementer classes.

APIs Related to Sealed Classes and Interfaces

The class java.lang.Class has two new methods related to sealed classes and interfaces:

  • java.lang.constant.ClassDesc[] permittedSubclasses(): Returns an array containing java.lang.constant.ClassDesc objects representing all the permitted subclasses of the class if it is sealed; returns an empty array if the class is not sealed.
  • boolean isSealed(): Returns true if the given class or interface is sealed; returns false otherwise.

Source: https://docs.oracle.com/sealed-classes-and-interfaces.html

Considerations When Using Sealed Classes and Interfaces

While sealed classes and interfaces offer many advantages, there are some considerations to keep in mind:

1. Design Decisions: Carefully consider which classes or interfaces should be sealed and which should remain open for extension. Overusing sealed classes can lead to rigid hierarchies, while underusing them may not provide the desired level of control.

2. Compatibility: Sealing a class or interface can impact backward compatibility. Once a class or interface is sealed, adding new permitted subclasses or implementers in future versions of your code can break existing code that depends on the sealed API.

3. Package Organization: To use sealed classes effectively, we must organize our classes in the same package or module. This can influence our project’s package structure and organization.

FAQ

What are the primary goals behind the introduction of sealed class in Java?

1. There was no control on inheritance hierarchy. Any class could extend another class until we use the final keyword. In other words, we can only restrict a class from being extended using final keyword. Sealed classes can control which classes can extend it by including them in the permitted list.
2. On the other hand, a class can also have a control which classes will be its children.

Can a subclass of a sealed class in java be further sealed?

Yes, we can further seal a subclass of a sealed class. Sealing a subclass allows us to control which specific classes can extend the subclass, providing additional scalability within the inheritance hierarchy.

For example, below code demonstrates the concept:

public sealed class Vehicle permits Car, Motorcycle {
// Vehicle class members
}

public sealed class Car extends Vehicle permits Sedan, SUV {
// Car class members
}

public sealed class Sedan extends Car {
// Sedan-specific members
}

public final class SUV extends Car {
// SUV-specific members
}

public sealed class Motorcycle extends Vehicle permits SportBike, Cruiser {
// Motorcycle class members
}

public final class SportBike extends Motorcycle {
// SportBike-specific members
}

public final class Cruiser extends Motorcycle {
// Cruiser-specific members
}

In this example, Vehicle is a sealed class that permits Car and Motorcycle as subclasses. Car is a sealed class that further permits Sedan and SUV as its subclasses. Sedan and SUV are both subclasses of Car, but Sedan is further sealed, while SUV is marked as final. Similarly, Motorcycle is a sealed class that permits SportBike and Cruiser as its subclasses. SportBike and Cruiser are subclasses of Motorcycle, and both are marked as final.

Leave a Reply


Top