167. Introducing JDK 17 Sealed Classes
Among the cool features of JDK 17, we have JEP 409 (Sealed Classes). This JEP provides an explicit, intuitive, crystal clear solution for nominating who will extend a class/interface or will implement an interface. In other words, Sealed Classes can control inheritance at a finer level. Sealed Classes can affect classes, abstract classes, and interfaces and sustain the readability of the code – you have an easy and expressive solution to tell your colleagues who can extend/implement your code.

Figure 8.3 – JDK 17, JEP 409
Via Sealed Classes we have finer control over a hierarchy of classes. Sealed Classes provide the granularity that we cannot obtain via the final modifier and package-private access.
Sealed Classes don’t affect the semantics of the final or abstract keywords. They are acting exactly as we know for years. A sealed class cannot be final and vice-versa.
Let’s consider the following class (Truck.java):
public class Truck {}
We know that, in principle, this class can be extended by any other class. But, we have only three types of trucks: semi-trailer, tautliner, and refrigerated. So, only three classes should extend the Truck class. Any other extension should not be allowed. In order to achieve this goal, we seal the class Truck by adding in its declaration the sealed keyword as follows:
public sealed class Truck {}
By adding the sealed keyword the compiler will automatically scan for all the extensions of Truck predefined in Truck.java.Next, we have to specify the subclasses of Truck (SemiTrailer, Tautliner, and Refrigerated).
A sealed class (abstract or not) must have at least a subclass (otherwise, there is no point to declare it sealed). A sealed interface must have at least a subinterface or an implementation (otherwise, there is no point to declare it sealed). If we don’t follow these rules then the code will not compile.
If we declare the subclasses of Truck in the same source file (Truck.java) then we can do it as follows:
final class SemiTrailer extends Truck {}
final class Tautliner extends Truck {}
final class Refrigerated extends Truck {}
After checking this code we have to push here another important note.
A subclass of a sealed class must be declared final, sealed, or non-sealed. A subinterface of a sealed interface must be declared sealed or non-sealed. If the subclass (subinterface) of a sealed class (interface) is declared as sealed then it must have its own subclasses (subinterfaces). The non-sealed keyword indicates that the subclass (subinterface) can be freely extended further with no restrictions (the hierarchy containing a non-sealed class/interface is not closed). And, a final subclass cannot be extended.
Since our subclasses (SemiTrailer, Tautliner, and Refrigerated) are declared final they cannot be extended further. So, the Truck class can be extended only by SemiTrailer, Tautliner, and Refrigerated, and these classes are non-extendable.In the case of interfaces, we do the same. For instance, a sealed interface looks like this:
public sealed interface Melon {}
By adding the sealed keyword the compiler will automatically scan for all the implementations/extensions of Melon predefined in Melon.java. So, in the same source file (Melon.java), we declare the extensions and implementations of this interface:
non-sealed interface Pumpkin extends Melon {}
final class Gac implements Melon {}
final class Cantaloupe implements Melon {}
final class Hami implements Melon {}
The Pumpkin interface can be further freely implemented/extended since it is declared as non-sealed. The implementations/extensions of Pumpkin don’t require to be declared sealed, non-sealed, or final (but, we can do it).Next, let’s have a more complex example (let’s name this model the Fuel model). Here, all classes and interfaces are placed in the same source file, Fuel.java (package, com.refinery.fuel). Take your time and analyze each class/interface to understand how sealed, non-sealed, and final work together in this hierarchal model:

Figure 8.4 – A hierarchical model using sealed, non-sealed, and final
In code lines, this model can be expressed as follows:
public sealed interface Fuel {}
sealed interface SolidFuel extends Fuel {}
sealed interface LiquidFuel extends Fuel {}
sealed interface GaseousFuel extends Fuel {}
final class Coke implements SolidFuel {}
final class Charcoal implements SolidFuel {}
sealed class Petroleum implements LiquidFuel {}
final class Diesel extends Petroleum {}
final class Gasoline extends Petroleum {}
final class Ethanol extends Petroleum {}
final class Propane implements GaseousFuel {}
sealed interface NaturalGas extends GaseousFuel {}
final class Hydrogen implements NaturalGas {}
sealed class Methane implements NaturalGas {}
final class Chloromethane extends Methane {}
sealed class Dichloromethane extends Methane {}
final class Trichloromethane extends Dichloromethane {}
Placing all the classes/interfaces in the same source file allows us to express closed hierarchical models as the previous one. However, placing all classes and interfaces in the same file is rarely a useful approach – maybe when the model contains a few small classes/interfaces.In reality, we like to separate classes and interfaces in their own source files. It is more natural and intuitive to have each class/interface in its own source file. This way we avoid large sources and is much easier to follow the best practices of OOP. So, the goal of our next problem is to rewrite the Fuel hierarchical model by using a source file per class/interface.