Press "Enter" to skip to content

Effectively Sealed Classes in Java

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

In this short article, we’ll see how to achieve the semantics of the sealed types in Java…before the language actually 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 own extensions.

Furthermore, we could leverage additional compiler support, for example, 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 in a totally different way 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 really 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'

And this is how we end 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 for achieving the same as well – 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: