Home > Net >  Java 17 Sealed Classes Business Use Case
Java 17 Sealed Classes Business Use Case

Time:01-29

Java 17 has introduced sealed classes which can permit only specific classes to extend them and would otherwise be final

I understand the technical use-case, but can't think of any real life use cases where this would be useful?

When would we want only specific classes to be able to extend a particular class?

In our own projects, if we want a new class to extend the sealed class can't we just add it to the permitted classes? Wouldn't it be better to just not make the class final or sealed in that case to avoid the slight overhead?

On the other hand, while exposing a library for external use how would a sealed class know beforehand which classes it should permit for extension?

CodePudding user response:

sealed classes provide the opposite guarantee to open classes (the default in Java). An open class says to implementors "Hey, you can subclass me and override my non-final methods", but it says to users "I have no idea what subclasses look like, so you can only use my own methods directly". On the flipside, sealed classes are very restrictive to implementors "You cannot subclass me, and you can only use me in these prescribed ways", but very powerful to users: "I know in advance all of my subclasses, so you know that if you have an instance of me, then it must be one of X, Y, or Z". Consequently, adding a subclass to a sealed class is a breaking change.

It may be helpful to think of sealed classes less as "restricted classes" and more as "enums with superpowers". An enum in Java is a finite set of data values, all constructed in advance. A sealed class is a finite set of classes that you set forth, but those classes may have an infinite number of possible instances.

Here's a real-world example that I wrote myself recently. This was in Kotlin, which also has sealed classes, but it's the same idea. I was writing a wrapper for some Minecraft code and I needed a class that could uniformly represent all of the ways you can die in Minecraft. Long story short, I ended up partitioning the death reasons into "killed by another living thing" and "all other death reasons". So I wrote a sealed interface CauseOfDeath. Its two implementors were VanillaDeath (which took a "cause of damage" as its constructor argument, basically an enum listing all of the possible causes) and VanillaMobDeath (which took the specific entity that killed you as its constructor argument).

Since this was clearly exhaustive, I made it sealed. If Minecraft later adds more death reasons, they will fit into one of the two categories ("death by entity" or "death by other causes"), so it makes no sense for me or anyone else to ever subclass this interface again.

Further, since I'm providing very strong guarantees about the type of death reason, it's reasonable for users to discriminate based on type. Downcasting in Java has always been a bit of a code smell, on the basis that it can't possibly know every possible subclass of a class. The logic is "okay, you've handled cases X and Y, but what if someone comes along and writes class Z that you've never heard of". But that can't happen here. The class is sealed, so it's perfectly reasonable for someone to write a sort of pseudo-visitor that does one thing for "death by entity" and another for "death by other", since Java (or Kotlin, in my case) can be fully confident that there are not, and never will be, any other possibilities.

This makes more sense as well if you've used algebraic data types in Haskell or OCaml. The sealed keyword originated in Scala as a way to encode ADTs, and they're exactly what I just described: a type defined as the (tagged) union of a finite number of possible collections of data. And in Haskell and OCaml, it's entirely normal to discriminate on ADTs as well using match (or case) expressions.

  • Related