decorative image for blog on sealed classes in Java
September 17, 2020

Exploring Sealed Classes in Java

Java Updates
Java Application Development

Sealed classes, as a preview feature in Java 15, offer a way to declare all available subclasses of a class or interface. But how will that impact developers in their work?

In this blog, we look at sealed classes, how they work, their benefits, and how they may be used by developers working with Java 15.

What Is a Sealed Class?

As stated in the JEP 360 notes, sealed classes and interfaces restrict which other classes or interfaces may extend or implement them.

In other words, sealed classes help to make superclasses accessible while limiting unintended extensibility.

Why Are Sealed Classes Important?

So why is it useful to give more precision in declaring available subclasses within a class? Primarily, it simplifies code by providing the option to represent the constraints of your domain. Developers don’t need to use a catch-all else block, or a default section in a switch, to avoid getting an unknown type.

The latter point is important when considering how pattern matching can be implemented in the future. There, sealed classes can be read as exhaustive by the javac compiler.

Additionally, it helps for serialization of data to and from structured data formats, like XML. Using sealed classes allows developers to know all possible subtypes are supported in the given format.

How Do Sealed Classes Work?

For an example of sealed classes, we can look at a simple calculator. This calculator allows you to do basic mathematical expressions, like multiplication, division, addition, subtraction. By using a sealed class, you can declare a BinaryExpression interface, with explicitly permitted implementations. In this case, we’ll explicitly declare the four mathematical expressions mentioned above.

public sealed interface BinaryExpression
    permits Addition, Subtraction, Division, Multiplication {
    double perform(double x, double y);
}
public final class Addition implements BinaryExpression {
    @Override
    double perform(double x, double y) {
        return x + y;
    }
}
/* etc */

The sealed keyword indicates that this interface is sealed, and the permits clause the list all direct permitted subtypes. The individual subtypes must then themselves be either sealed, final or non-sealed, where the latter – one of the new hyphenated keywords – indicates that that part of the sub-tree is open to be freely subtyped.

Having part of the tree open to be freely subtyped does not impact the future exhaustive check in the javac compiler, as those will still all be subtypes of a type declared in the permits clause.

Using Sealed Classes With Records

While the previous example has a method taking two arguments, it could just as well be implemented using an interface like:

public sealed interface Expression {
  double evaluate();
}

Using records, which is in its seconds preview in Java 15, we can take advantage of them being final by design, and use them to implement our basic mathematical expressions. We further expand it with constant expression and unary expressions. For this example, the UnaryExpression has been opened up, thus allowing further subclasses to exist than defined here. For instance would allow for a trigonometry extension to introduce sin, cos, and tan expressions.

public sealed interface Expression
    permits ConstantExpression, UnaryExpression, BinaryExpression {
  double evaluate();
}

public non-sealed interface UnaryExpression extends Expression {}

public sealed interface BinaryExpression extends Expression
    permits Addition, Subtraction, Division, Multiplication {}

public record ConstantExpression(double value)
    implements Expression {
  public double evaluate() { return value; }
}

public record Negative(Expression x)
    implements UnaryExpression {
  public double evaluate() { return -x.evaluate(); }
}

public record Addition(Expression x, Expression y)
    implements BinaryExpression {
  public double evaluate() { return x.evaluate() + y.evaluate(); }
}

public record Subtraction(Expression x, Expression y)
    implements BinaryExpression {
  public double evaluate() { return x.evaluate() - y.evaluate(); }
}

public record Division(Expression x, Expression y)
    implements BinaryExpression {
  public double evaluate() { return x.evaluate() / y.evaluate(); }
}

public record Multiplication(Expression x, Expression y)
    implements BinaryExpression {
  public double evaluate() { return x.evaluate() * y.evaluate(); }
}

How Can Sealed Classes Be Applied?

While most of us probably won’t implement a calculator, the example does show how we can use sealed types to declare the constraints within a domain.

Sealed classes could also provide an additional protection against initialization of unintended classes during polymorphic deserialization in unmarshalling frameworks, such as Jackson. Polymorphic deserialization has been one of the primary attack vectors in such frameworks. These frameworks could take advantage of knowing the complete set of subtypes, and abort before even trying to load the class.

Final Thoughts

In this blog, we looked at how sealed classes work, and how they may be applied practically by developers.

While sealed classes may not drive adoption of Java 15 on their own, they do serve as an incremental, quality of life improvement for developers working with the language.

Thanks for reading, and be sure to view the resources below if you're looking for more on the latest in Java.

Additional Resources

Want to get insight into recently released features like Text Blocks and Switch Expressions? This webinar below is a good place to start.

Looking for further insight into new features in Java? Be sure to check out the resources below.

Want to Speed Up Java Development?

With JRebel, your team can skip redeploys and get back to doing what they do best — coding! See how JRebel works on your project today with a free, 10-day trial

Try JRebel Free