Sunday, 1 September 2024

Understanding Sealed Class in Java and its use-case


Introduction to Sealed Class and Interfaces

Sealed Class in Java are used to allow / permit specific named Class which can extend the Sealed Class. 
With Sealed Class, we can say that Class A can only be extended by Class B and Class C and not any other class, thus limiting who can extend a particular Java Class



Detailed Explanation and use-case

Let's start with a very simple example. Consider we are creating a base class name Animal which implements basic feature of animal like walking, running etc
Now, We want few classes to extend Animal class like Cat, Dog, Lion, Tiger etc which we can do it easily and implement specific respective feature
Since this Animal Class can be extended by any class, someone can accidentally extend Animal class for Pigeon too which is not correct
 In other words, Sealed Class / Interface permits (or restricts) which all Class / Interface can use them. It prevents misuse of Java Class / Interface

Advantage and Disadvantage of using Sealed Class

Lets discuss Advantage first

Advantage of Sealed Class and Interface:

  • The developer of Sealed Class can manage who can use / or whose code can implement it
  • If we compare with access modifier, Sealed Class provide a more declarative way to restrict use of super class
  • Sealed Class and its permitted sub-class can be used as Pattern Matching using Switch statement

Disadvantage of Sealed Class and Interface

  • If not properly designed / thought about its all permitted sub-class, need to revisit code of super class to add more permitted sub-class
  • In few use-cases, unit testing would be difficult
  • Permitted class uses cannot be detected at compile time, so if it has been used and its not in list of permitted sub-class, it cannot throw compile time error
  • Limitation of usage of permitted sub-class within same module only

How to declare Sealed Class:

Sealed class can be declared with using the keyword sealed along with its permit sub-classes with keyword permits

Example of How to create Sealed Classes:

package com.tech693.java.examples.seal;

public sealed class Animal permits Cat, Dog {

    public void getIdentity(){

        System.out.println("Animal");
    }
}

Notice the sealed and permits keywords. Its mandatory to declare one sub-class using permits keyword when using sealed. 
By this we are declaring that only Cat and Dog sub-class can extend the Animal class and no other class is allowed to extend Animal class
Now, lets see how to create Cat and Dog class

Example of how to create Sealed sub-class:

package com.tech693.java.example.seal;

// Pls note its mandatory to declare sub class as either sealed or non-sealed or final
public non-sealed class Cat extends Animal { 
    
    public void getIdentity(){
        System.out.println("Cat");
    }    
}

package com.tech693.java.example.seal;

// Pls note its mandatory to declare sub class as either sealed or non-sealed or final
public final class Dog extends Animal {
    
    public void getIdentity(){
    System.out.println("Dog");
    }  
}

Also, the sub-class should directly extend the sealed class.
for eg.  Suppose, A is a sealed class which permits B to extend it and B is also a sealed class that permits C. In this scenario C cannot directly extend A

Its mandatory to declare sub class as either sealed or non-sealed or final which has different relevance
  • Sub-class as Sealed: - Can only be further extended by sub-class permitted class
  • Sub-class as final :- Cannot be further extended
  • Sub-class as non-sealed - Can be extended further by any class
    • The idea to have non-sealed is to know the developer that it extends the sealed class and is not accidently allowing further extended by its sub-class
Now lets run the above code using Main class as shown below:
package com.tech693.java.example.seal;

public class SealedClassExample {
    public static void main(String[] args) {
        Animal animal = new Cat();
        animal.getIdentity();

        Animal animal1 = new Dog();
        animal1.getIdentity();
    }
}

Output

So when Animal class is declared as cat, it will print Cat when getIdentity() is called

Here is the output of the above code :

Output:

Cat
Dog

Sealed Interface:

Sealed interface acts similarly as Sealed Class. The only difference is the difference between abstract class and interface (we have have some business logic code in abstract class but cannot have it in interface)

Sealed Interface Example:

Let's see some code example of implementing sealed as Interface
package com.tech693.java.examples.seal;

public sealed interface Animal permits Cat, Dog {

    public void getIdentity(){}
}

package com.tech693.java.example.seal;

// Pls note its mandatory to declare sub class as either sealed or non-sealed or final
public non-sealed class Cat implements Animal { 
    
    public void getIdentity(){
        System.out.println("Cat");
    }    
}

package com.tech693.java.example.seal;

// Pls note its mandatory to declare sub class as either sealed or non-sealed or final
public final class Dog implements Animal {
    
    public void getIdentity(){
    System.out.println("Dog");
    }  
}


Till now we have got the basic idea about Sealed Classes and its permitted sub-class.

Lets understand further in more depth about its use-case and how Java implemented Sealed class

Record class as permitted subclasses of a Sealed Class

Since Record class are implicitly final we can use record class in the permits clause of a Sealed Class or Interface
Here is an example of Record class as subclasses of a sealed class

package com.example.records.expressions;

sealed interface Expr permits ConstantExpr, PlusExpr, TimesExpr, NegExpr {
    public int eval();
}

record ConstantExpr(int i) implements Expr {
    public int eval() { return i(); }
}

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

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

record NegExpr(Expr e) implements Expr {
    public int eval() { return -e.eval(); }
}
Note that we haven't declare any modifier (final, non-sealed or sealed) to sub-class. This is because Record class are final by default.

Design considerations when using Sealed Classes and Interfaces

We should keep in mind following consideration while designing Sealed Classes or Interface
  • Design Decision - We should carefully consider which class should be declared sealed before start implementing only. Sealed class / interface consideration often results in tightly coupled design instead of loosely coupled
  • Backward Compatibility - If we plan to use sealing class, it can impact backward compatibility. Adding new permitted sub-class in future release can break existing code that depends on the sealed API
  • Package / Module Arrangement - We should carefully construct Java Module / Package structure when used sealed classes or interface as the scope of sealed class and its permitted sub-class is scoped under a single module.

Sealed Class as Pattern Matching case:

This is very important to understand. The introduction of Sealed class in Java opens up a new paradigm of using Permitted classes as Pattern Matching in switch case
Let's first understand how Sealed Class is implemented in JDK. 
Although sealed is a class modifier, there is no ACC_SEALED flag in the ClassFile structure. Instead, the class file of a sealed class has a PermittedSubclasses attribute which implicitly indicates the sealed modifier and explicitly specifies the permitted subclasses:

PermittedSubclasses_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 number_of_classes;
    u2 classes[number_of_classes];

Code Example: Consider above example of Animal as sealed class and Cat and Dog as its permitted sub class. 
Here is the example where we can use sealed class in switch case

private static String checkAnimal(Animal animal){                                     
    return switch (animal) {                                                          
        case Cat c -> c.getIdentity();                                                
        case Dog d -> d.getIdentity();                                                
        default -> throw new IllegalArgumentException("Unexpected Input: " + animal); 
    };                                                                                
}


Frequently Asked Question:

Why Sealed Class is introduced in Java, what problem does it solves?
 
The purpose of sealed class is to have more control on sub-class / interface hierarchy. It provides a way to create hierarchy of classes and its sub-classes. It also solves the problem of using sealed class as Pattern Matching using switch case

What is the difference between sealed and final class?

Final class means no other class can extend it whereas sealed class means only permitted sub-class can extend it

What is the difference between sealed and abstract class?


Sealed class can be used along with abstract keyword and its a good idea because in most of the use-case sealed class is abstract and its permitted sub-class has actual implementation. for eg - Shape sealed class can have Rectangle, Triangle as its permitted sub-class and the actual implementation of its area, radius, circumference and diameter code is in its permitted sub-class