Sealed Classes - A Java 17 New Feature

Introduction

In this article, we will be discussing one new feature of Java i.e introduced in Java 17. Before moving forward to know more about sealed classes, we already know about abstract classes and final classes.

When it comes to abstract classes we create a base class which may have some abstract methods and also some defined methods, actual implementation will be done by concrete classes which will have all the implementation. In short, abstract classes are meant to be inherited.

When it comes to final classes which means no other class can inherit this is the final implementation of this type of classes we can’t inherit it. 

It provides us a standard way to work but what we want is something in between. We want to have inheritance but not every class can do it. There is some limited class which we want it to inherit. So here comes the concept called sealed classes where we can mention which are subclasses or sub interfaces which can inherit from this particular class and interface and we can do this with the help of sealed keyword.

Why Sealed Classes?

To understand more about sealed class we should first know why it is required. If we want to restrict the subclasses for a particular class we can make it sealed, this is only applicable for designing an application.

Example

Suppose we are building a payment-service and we want to allow a selective payment gateway to be used for payments. 

As a developer we should write code that actually means what it is intended for, we don’t want to write code that is not intended. There are times when we want to really get control over representing a hierarchy of a class but in object oriented programming especially in functional programming we often represent categories of types of data and there are times when we model our class based on what our domain requires based upon the requirements.

To control the hierarchy of classes in our application as it will result in reducing complexity, make code easier to write, prevent compiler errors as it is not done properly. Previously we used to define a constructor as a package protected that can only be accessed by class within the same package. 

Let us understand this scenario in more detail, so we create a base class with a protected constructor and derived classes would be part of the same package, so we can control it as an author of the class. We can also create a new class within the same package so as a result there is no good control. But we have used another approach, not a better one but we have used ENUMS. As we know, java internally treats Enums as a class and Enums gives us a closed hierarchy. So to overcome this we have a sealed feature that helps to reveal the intention of the code, when we use the word sealed we are conveying our intention of creating a closed hierarchy. 

How does it work with Java?
 

With Interface

Let us create a sealed class whether it is a class or an interface, we can mark them as sealed. If it is sealed interface then we are conveying that hierarchy below that interface is closed, if it is a sealed class then the hierarchy below that class is closed, so we can use either class or interface as a sealed.

Let us start creating a sealed interface. As we know interfaces are for extending or implementing but making it sealed conveys to the compiler that we want to define a closed hierarchy. 

public sealed interface Light {}

With the interface we defined a class called RedLight and that implements Light interface and similarly YellowLight and GreenLight. 

final class RedLight implements Light{}
final class YellowLight implements Light{}
final class GreenLight implements Light{}

Let us create the instance of any of this class,

public class SealedDemo {
    public static void main(String[] args) {
        Light light = new RedLight();
        System.out.println("OK");
    }
}
Till now the compiler is happy and what we achieve by doing this is we create a closed hierarchy of the interface. In this case, the three classes implementing the interface all reside in the same file and that fact we say sealed, the compiler goes to check all the possible implementations that’s been predefined in this particular file.
public sealed interface Light {}
final class RedLight implements Light {}
final class YellowLight implements Light {}
final class GreenLight implements Light {}
public class SealedDemo {
    public static void main(String[] args) {
        Light light = new RedLight();
        System.out.println("OK");
    }
}

Now we define a new class called BrokenLight that implements Light.

class BrokenLight implements Light{}

So now when we compile this code we get an error saying 

error: class is not allowed to extend a sealed class.

Now we know it is an interface but it is sealed so we cannot extend from it and the reason why we are not able to extend is because a closed hierarchy is defined inside one file this is the simplest behavior of the sealed modifier.

Now we define a new class called FlashLight that implements Light.

class FlashLight implements Light{}

This is the same as we did in the earlier step it will not work but what if we define the interface and extends that we want to specify by adding extends or permits.

public sealed interface Light permits RedLight, YellowLight, FlashLight {}

This way we can allow class FlashLight to implement the desired interface and now compiler will allow us to create class FlashLight without any error.

To sustain the principle of closed hierarchy we will use the final keyword with FlashLight class so that others cannot implement or extend it.

final class FlashLight implements Light{}

With Classes

Previously we understood sealed modifier when used with an interface, here we will be discussing it with classes. 

Suppose we have four classes named University, EngineeringCollege, DegreeCollege, School. 

class University {}
class EngineeringCollege {}
class DegreeCollege {}
class School {}

Here only class EngineeringCollege and DegreeCollege should be able to inherit by class University, class School should not be allowed in it, this thing is possible by using sealed modifier. 

When we use sealed it normally permits some class to inherit so if we want to allow EngineeringCollege and DegreeCollege but not School we can do it here only we have to mention permits after the class name like,

sealed classUniversitypermitsEngineeringCollege, DegreeCollege {}

So here JVM allows class University to be inherited by EngineeringCollege and DegreeCollege only. 

Important Points

  1. The permitted class should have one of these three things i.e non-sealed, sealed or final.
  2. We need to directly extend or implement from a sealed class or an interface respectively. So if we want to create a closed hierarchy we need to start implementing these classes or interfaces. 
  3. Sealed Classes must have a subclass.

Advantages of Sealed Classes

Better Modeling

This means we are able to represent the intention of what we are trying to convey from our object modeling, this way we can make our code very intentional. 

Better Compiler Checks

We had seen this already in a couple of different situations. We saw how we are getting compilation errors when we create a hierarchy but when we create a class that was not a part of hierarchy it shows the “error: class is not allowed to extend a sealed class.” because it is not a member of a closed hierarchy. 

Enhanced Pattern Matching

In this compiler can verify all the possible scenarios. If we are not checking all scenarios, the compiler will throw an error.

Let us see by an example, we have created an enum for HttpState with some values. We will check if it covers all scenarios or not.

enum HttpStateCodes {
    OK,
    CREATED,
    NOT FOUND,
    INTERNAL SERVER ERROR
};
public static String checkCodes(HttpStateCodes code) {
    return switch (code) {
        case 1:
            "got OK";
        case 2:
            "got CREATED";
        case 3:
            "got NOT FOUND";
        case 4:
            "got INTERNAL SERVER ERROR";
    }
}
public class SealedDemo {
    public static void main(String[] args) {
        Light light = new RedLight();
        System.out.println("OK");
    }
}

Here it will work as expected as we are covering all the possible scenarios but what if we failed to check for one case mentioned in enum class. 

public static String checkCodes(HttpStateCodes code) {
    return switch (code) {
        case 1:
            "got OK";
        case 2:
            "got CREATED";
        case 3:
            "got INTERNAL SERVER ERROR";
    }
}

Now if we compile it will throw a compilation error saying,

error: the switch expression does not cover all the possible input values.

Summary

With this we came to the last of this article, we had a lot of new learnings in this article. But before finishing, we will summarize what we learned. 

What did we learn?

  • What are Sealed Classes/ Interfaces?
  • Why do we need Sealed Classes?
  • How does it work with Java when used with Interface and Classes
  • Advantage of Sealed Classes.