Sealed classes and interfaces in Java

Java is best known as an object-oriented programming language, offering features like inheritance to enhance code reusability. However, until Java 17, there was no way to control inheritance. With the introduction of sealed classes and interfaces, Java now allows developers to specify which classes can extend or implement them.

Ivan Polovyi
Javarevisited
6 min readJun 13, 2024

--

The problem

First, we must define a problem that sealed classes and interfaces were created to solve. Let's imagine this simple inheritance:

In this example, we have an abstract class Transport and two concrete classes, Car and Bus, which extend the Transport class.

The current structure provides a foundation for additional logic and domain-specific implementations. For instance, classes like Airplane and Ship could be added to represent different modes of transport. However, without the new features introduced in Java 17, unconventional implementations like a Bike could also be introduced, but these might not align with the intended hierarchy.

Java 17 introduces sealed classes and interfaces, offering a solution to this issue. These features allow us to control which classes can extend or implement them, providing a more structured and secure approach to class hierarchies.

sealed, non-sealed, and permits

Java 17 introduced three new contextual keywords: sealed, non-sealed, and permits. The words sealed and permits have special meanings only when defining sealed types and are used in the class or interface header.

acessor sealed class ClassName permits Subclass {}

acessor sealed interface InterfaceName permits Subclass {}

The non-sealed keyword can only be applied to a subclass of a sealed superclass, a class implementing a sealed interface, or an interface extending a sealed interface.

acessor non-sealed class Subclass extends ClassName {}
acessor non-sealed class Subclass implements InterfaceName {}
acessor non-sealed interface extends InterfaceName {}

Sealed classes

Let’s make our class Transport sealed.

package com.polovyi.ivan.tutorials;

public abstract sealed class Transport permits Car, Bus, Ship {

private String name;

private int capacity;

public Transport(int capacity, String name) {
this.capacity = capacity;
this.name = name;
}
}

Now, our class is sealed and can be extended only by the classes specified in the permits clause. These classes must exist, extend the Transport class, and be accessible to it; otherwise, the compiler will produce an error.

Classes that extend a sealed class can be either final, sealed, or non-sealed. If a subclass is final, it cannot be further extended, guaranteeing that our sealed class won't have any additional subclasses.

public final class Car extends Transport {
public Car(int capacity, String name) {
super(capacity, name);
}
}

A subclass of a sealed class can also be a sealed class, allowing it to control its own inheritance.

package com.polovyi.ivan.tutorials;

public sealed class Bus extends Transport permits Minivan {
public Bus(int capacity, String name) {
super(capacity, name);
}
}

And its subclass:

package com.polovyi.ivan.tutorials;

public final class Minivan extends Bus {
public Minivan(int capacity, String name) {
super(capacity, name);
}
}

Lastly, a subclass of a sealed class can be a non-sealed class:

package com.polovyi.ivan.tutorials;

public non-sealed class Ship extends Transport {
public Ship(int capacity, String name) {
super(capacity, name);
}
}

This class can be extended by any subclass:

package com.polovyi.ivan.tutorials;

public class Cruise extends Ship {

public Cruise(int capacity, String name) {
super(capacity, name);
}
}

If we attempt to extend the Transport class with a Bike class, the compiler will throw an error, indicating that it is not allowed because the Bike class is not listed in the permits clause.

package com.polovyi.ivan.tutorials;

public class Bike
// extends Transport // won't compile
{ }

Now, we can represent the relationships of classes using the bellow diagram:

The permits clause can be omitted when the superclass and its subclasses are declared in the same file:

package com.polovyi.ivan.tutorials;

public sealed class SameCompileUnit {}

final class Subclass1 extends SameCompileUnit {}

non-sealed class Subclass2 extends SameCompileUnit {}

sealed class Subclass3 extends SameCompileUnit {}

final class SubclassOfSubclass3 extends Subclass3 {}

Sealed interfaces

Interfaces can also be sealed. Unlike sealed classes, sealed interfaces can have both classes and interfaces as subtypes.

A subtype of a sealed interface must be one of the following: sealed, non-sealed, or final class or a sealed or non-sealed interface. As you already know, an interface cannot be declared as final.

Let’s slightly modify our example. Imagine we have an interface called Transport that has a single method, generateTicketPrice().

package com.polovyi.ivan.tutorials.interfaces;

public interface Transport {
double generateTicketPrice();
}

Now, we can make it sealed and specify which classes and interfaces are allowed to implement or extend it.

package com.polovyi.ivan.tutorials.interfaces;

public sealed interface Transport permits Car, Bus, Ship {
double generateTicketPrice();
}

The diagram will look like below;

The Car class implements the Transport interface and is marked as final, so it can't be extended. If we remove the final modifier, the compiler will flag an error.

package com.polovyi.ivan.tutorials.interfaces;

public final class Car implements Transport {

@Override
public double generateTicketPrice() {
return 10;
}
}

The Bus interface extends the Transport interface and is also sealed, so we must specify the permitted classes.

package com.polovyi.ivan.tutorials.interfaces;

public sealed interface Bus extends Transport permits Minivan {
}

Now, this interface can be implemented by only one class, which is marked as final.

package com.polovyi.ivan.tutorials.interfaces;

public final class Minivan implements Bus {
@Override
public double generateTicketPrice() {
return 2;
}
}

The Ship interface is non-sealed,

package com.polovyi.ivan.tutorials.interfaces;

public non-sealed interface Ship extends Transport {}

allowing any class or interface to implement or extend it.

package com.polovyi.ivan.tutorials.interfaces;

public interface Cruise extends Ship {}

Of course, the Bicycle interface cannot implement the Transport interface because it is not permitted.

package com.polovyi.ivan.tutorials.interfaces;

public interface Bicycle
// implements Transport // won't compile
{}

Similar to sealed classes, if a sealed interface is located in the same file as its subtypes (classes or interfaces), the permits clause can be omitted because the compiler can infer the permitted subtypes.

package com.polovyi.ivan.tutorials.interfaces;

public sealed interface SameCompileUnit {}

final class Subclass1 implements SameCompileUnit {}

non-sealed interface Subclass2 extends SameCompileUnit {}

sealed interface Subclass3 extends SameCompileUnit {}

final class SubclassOfSubclass3 implements Subclass3 {}

Enums

In Java, an enum can implement interfaces, including sealed interfaces. When an enum implements a sealed interface, it becomes one of the permitted types for that interface.

As an example, let's create the following sealed interface:

package com.polovyi.ivan.tutorials.enums;

public sealed interface Transport permits TransportType {
boolean hasWheels();
}

And an enum that implements it:

package com.polovyi.ivan.tutorials.enums;

public enum TransportType implements Transport {

CAR,
BUS;

@Override
public boolean hasWheels() {
return true;
}
}

Because an enum is implicitly final, specifying the final modifier is unnecessary. Since enums are final, they cannot be non-sealed. In fact, any type of modifier, such as sealed or non-sealed, is not permitted in the enum header.

Records

A Java record can implement a sealed interface, similar to an enum because it is implicitly final. This allows records to implement sealed interfaces straightforwardly.

package com.polovyi.ivan.tutorials.records;

public sealed interface Transport permits Starship {
boolean canTravelToTheMoon();
}

Let's create a record that implements it.

package com.polovyi.ivan.tutorials.records;

public record Starship() implements Transport {
@Override
public boolean canTravelToTheMoon() {
return true;
}
}

The record header cannot specify the final modifier because records are implicitly final. Similarly to enums, records cannot be marked as sealed or non-sealed.

The complete project can be found here:

Conclusion

Sealed classes and interfaces offer developers a valuable tool for exerting control over class hierarchies in Java. Developers can create more secure and maintainable code by ensuring that only specified classes and interfaces can extend or implement sealed types. Additionally, the compiler can provide better analysis of potential issues, leading to improved code quality and reliability.

Thank you for reading! If you enjoyed this post, please like and follow it. If you have any questions or suggestions, feel free to leave a comment or connect with me on my LinkedIn account.

--

--

Ivan Polovyi
Javarevisited

I am a Java Developer | OCA Java EE 8 | Spring Professional | AWS CDA | CKA | DCA | Oracle DB CA. I started programming at age 34 and still learning.