Press "Enter" to skip to content

Effectively Sealed Classes in Java

Disclaimer: this article was written in 2018, when we could only dream about seeing sealed interfaces in Java. Sealed interfaces were introduced in JDK17 and should be preferred to the approach described in this article. This technique is still applicable to older versions of Java.

Java is missing various “hot” features from languages like Scala or Kotlin, but luckily, some of them can be simulated using existing features, such as sealed hierarchies (JEP-409).

In this short article, we’ll see how to achieve the semantics of the sealed types in Java…before the language gets it.

Sealed Classes

One of the most popular sealed hierarchies that exist out there is Option.

The classic implementation involves making Option a sealed abstract class and providing two subclasses: Some and None.

In Scala, it looks like:

sealed abstract class Option[+A] extends Product {}
 
case object None extends Option[Nothing] {}
case class Some[+A](value: A) extends Option[A] {}

If we try to extend Option further, it’s possible to add the extension to the file containing the base class – and that’s the essence of class sealing – which means that consumers of such API can’t create their extensions.

Furthermore, we could leverage additional compiler support, such as extra exhaustiveness checks, when working with Pattern Matching.

You can find it, for example, in Kotlin as well.

Sealed Classes in Java

Since we don’t have a dedicated solution, we need to try to construct our own.

In Java, Optional is implemented differently because it’s a potential candidate to become a value type in the future – but that’s another story for another time.

But now, let’s try to achieve sealed-class semantics using vanilla Java – the whole trick relies on a clever usage of visibility restrictions of private constructors and nested classes:

public abstract class Option<T> {
    // ...
    public final static class Some<T> extends Option<T> { ... }
    public final static class None<T> extends Option<T> { ... }
}

Unfortunately, we can’t forbid the class from being extended… but we can make every extension outside the file unusable by making the default constructor private:

public abstract class Option<T> {
    private Option() {}
    // ...
    public final static class Some<T> extends Option<T> { ... }
    public final static class None<T> extends Option<T> { ... }
}

And now, if we try to create an anonymous implementation, we end up with a compilation error:

Option<Integer> o = new Option<Integer>() {}
// 'Option()' has private access in 'com.pivovarit.sealed.Option'

And, if we try to create a standalone extension, it turns out that the default constructor isn’t visible from the outside:

public class SomeNone<T> extends Option<T> {
    private SomeNone() {
    }
}
// There is no default constructor available in 'com.pivovarit.sealed.Option'

This is how we ended up with an effectively sealed class in Java.

If we don’t really fancy keeping all our subclasses in a single file, we can leverage the package-private visibility modifier to achieve the same—if we go this way, we can keep all of them in a dedicated package.

Unfortunately, remember that this option doesn’t give us exhaustiveness checks.

A Complete Example

package com.pivovarit.sealed;

import java.util.function.Supplier;

public abstract class Option<T> {

    abstract T getOrElse(Supplier<T> other);

    private Option() {
    }

    public final static class Some<T> extends Option<T> {

        private final T value;

        public Some(T value) {
            this.value = value;
        }

        @Override
        T getOrElse(Supplier<T> other) {
            return value;
        }
    }

    public final static class None<T> extends Option<T> {
        @Override
        T getOrElse(Supplier<T> other) {
            return other.get();
        }
    }
}

The above code snippet can be found on GitHub.




If you enjoyed the content, consider supporting the site: