Why you should care about Sealed Types

by: Ethan McCue

As of Java 17 (with --enable-preview) you can combine two features - pattern matching and sealed types - to represent and deal with cases where two things have different sets of data.

Say you want to represent three different kinds of items you can check out from a library.

A book, which has a title and author

record Book(String title, String author) {}

A CD, which has a runtime and genre

record CD(double runtime, String genre) {}

And a VHS tape, which contains precious memories

record VHSTape(List<Memory> preciousMemories) {}

What you can do to have a method which might return any one of these cases, or to have a list of any item in the library, is make a common interface that all three of them share

interface LibraryItem {}

record Book(String title, String author) implements LibraryItem {}

record CD(double runtime, String genre) implements LibraryItem {}

record VHSTape(List<Memory> preciousMemories) implements LibraryItem {}

The problem is that if you have a LibraryItem object, there isn't much you can actually do with it since the three cases of library items have different kinds of data and thus you cant access everything from the interface

LibraryItem item = ...;
item.title(); // nope
item.runtime(); // also nope
item.preciousMemories(); // you'll never get them back

To remedy this, you can make the interface sealed. This guarantees to the language that Book, CD, and VHSTape are the only classes which implement LibraryItem

sealed interface LibraryItem permits Book, CD, VHSTape {}

Now if you have an instance of library item, you can use it with a "pattern switch expression" to safely recover the actual type of the item

LibraryItem item = ...;
switch (item) {
    case Book book ->
         System.out.println(book.title());
    case CD cd ->
         System.out.println(cd.genre());
    case VHSTape tape ->
         System.out.println(tape.preciousMemories());
}

<- Index