Java Sealed Classes and Interfaces: Controlling Inheritance for Better Code Design

Explore Java's Sealed Classes and Interfaces, introduced in Java 15 and made standard in Java 17. Learn how these features give developers fine-grained control over inheritance, allowing them to specify which subtypes can extend a class or interface for better security and code design.



Java - Sealed Classes and Interfaces

Java 15 introduced a sealed class as a preview feature, providing fine-grained control over inheritance. Java 16 enhanced this feature slightly and maintained it as a preview. By Java 17, sealed classes and interfaces became standard features. A sealed class or interface allows developers to control which subtypes can extend it, preventing unwanted inheritance.

Key Points of Sealed Classes

  • A sealed class is declared using the sealed keyword.
  • Sealed classes allow the declaration of permitted subtypes using the permits keyword.
  • A class extending a sealed class must be declared as either sealed, non-sealed, or final.
  • Sealed classes help create a finite and determinable hierarchy of classes in inheritance.

Sealed Interface

An interface can be marked as a sealed interface using the sealed keyword, and then the permits keyword can specify which interfaces can extend it.

Syntax

public sealed interface Person permits Employee, Manager {
}

Sealed Interface Example

In this example, we've created a sealed interface Person that permits the Employee and Manager interfaces to extend it. Each interface has different methods to retrieve the ID of a person. We use the instanceof operator to check the instance type and retrieve the corresponding ID.

Syntax

package com.tutorialsarena;

public class Tester {
public static void main(String[] args) {
    // create an instance of Manager
    Person manager = new CorpManager(23, "Robert");

    // get the id
    System.out.println("Id: " + getId(manager));
}

public static int getId(Person person) {
    // check if person is employee then return employee id
    if (person instanceof Employee) {
        return ((Employee) person).getEmployeeId();
    } 
    // if person is manager then return manager id
    else if (person instanceof Manager) {
        return ((Manager) person).getManagerId();
    }
    return -1;
}
}
Output

Id: 23

Sealed Class

Similar to sealed interfaces, a class can also be marked as a sealed class using the sealed keyword, followed by the permits keyword to specify which subclasses can extend this class.

Syntax

public abstract sealed class Person permits Employee, Manager {
}

Constraints of Sealed Classes

  • The permitted subclass must be part of the same module as the sealed class.
  • The permitted subclass must extend the sealed class.
  • The permitted subclass must use one of the following modifiers: final, sealed, or non-sealed.

Sealed Class Example

In this example, we've created a sealed abstract class Person that permits the Employee and Manager classes to extend it. Each class implements different methods to retrieve the ID of a person, utilizing the instanceof operator to determine the instance type.

Syntax

package com.tutorialsarena;

public class Tester {
public static void main(String[] args) {
    // create an instance of Manager
    Person manager = new Manager(23, "Robert");

    // get the id
    System.out.println("Id: " + getId(manager));
}

public static int getId(Person person) {
    // check if person is employee then return employee id
    if (person instanceof Employee) {
        return ((Employee) person).getEmployeeId();
    } 
    // if person is manager then return manager id
    else if (person instanceof Manager) {
        return ((Manager) person).getManagerId();
    }
    return -1;
}
}

// a sealed class Person which is to be inherited by Employee
// and Manager classes
abstract sealed class Person permits Employee, Manager {
String name;
String getName() {
    return name;
}
}

// Employee class has to extend Person and should have a modifier
final class Employee extends Person {
String name;
int id;

Employee(int id, String name){
    this.id = id;
    this.name = name;
}
int getEmployeeId() {
    return id;
}
}

// We can mark a sub-class as non-sealed, so that it can be extended if required
non-sealed class Manager extends Person {
int id;

Manager(int id, String name){
    this.id = id;
    this.name = name;
}
int getManagerId() {
    return id;
}
}
Output

Id: 23