53

I don't really understand why there is a non-sealed keyword in JEP 360/Java 15. For me, an extension of a sealed class should only be final or a sealed class itself.

Providing the "non-sealed" keyword will invite the developer for hacks. Why are we allowing a sealed class to be extended to a non-sealed class?

Brian Goetz
  • 90,105
  • 23
  • 150
  • 161
Ayox
  • 691
  • 5
  • 15
  • 3
    @Ayox take a look at this link: https://openjdk.java.net/jeps/360 *Motivation* part – Omid.N Sep 12 '20 at 12:14
  • Already read that (otherwise I wouldn't reference it myself) but I didn't understand it thats why I am asking. – Ayox Sep 12 '20 at 12:15
  • 3
    "Providing the "non-sealed" keyword will invite the developer for hacks." - Why do you think this is bad? What 'hacks' are you talking about? – Jorn Vernee Sep 12 '20 at 12:39

4 Answers4

76

Because in real-world APIs, sometimes we want to support specific extension points while restricting others. The Shape examples are not particularly evocative, though, which is why it might seem an odd thing to allow.

Sealed classes are about having finer control over who can extend a given extensible type. There are several reasons you might want to do this, and "ensuring that no one extends the hierarchy ever" is only one of them.

There are many cases where an API has several "built in" abstractions and then an "escape hatch" abstraction; this allows API authors to guide would-be extenders to the escape hatches that are designed for extension.

For example, suppose you have a system using the Command pattern, there are several built-in commands for which you want to control the implementation, and a UserPluginCommand for extension:

sealed interface Command
    permits LoginCommand, LogoutCommand, ShowProfileCommand, UserPluginCommand { ... }

// final implementations of built-in commands

non-sealed abstract class UserPluginCommand extends Command {
    // plugin-specific API
}

Such a hierarchy accomplishes two things:

  • All extension is funneled through the UserPluginCommand, which can be designed defensively for extension and provide an API suited to user extension, but we can still use interface-based polymorphism in our design, knowing that completely uncontrolled subtypes will not appear;

  • The system can still rely on the fact that the four permitted types cover all implementations of Command. So internal code can use pattern matching and be confident in its exhaustiveness:

switch (command) {
    case LoginCommand(...): ... handle login ...;
    case LogoutCommand(...): ... handle logout ...;
    case ShowProfileCommand(...): ... handle query ...;
    case UserPluginCommand uc: 
        // interact with plugin API
    // no default needed, this switch is exhaustive

There may be a zillion subtypes of UserPluginCommand, but the system can still confidently reason that it can cover the waterfront with these four cases.

An example of an API that will take advantage of this in the JDK is java.lang.constant, where there are two subtypes designed for extension -- dynamic constants and dynamic callsites.

armandino
  • 17,625
  • 17
  • 69
  • 81
Brian Goetz
  • 90,105
  • 23
  • 150
  • 161
23

I think the following example from the JEP 360 is showing it:

package com.example.geometry;

public abstract sealed class Shape
    permits Circle, Rectangle, Square {...}

public final class Circle extends Shape {...}

public sealed class Rectangle extends Shape 
    permits TransparentRectangle, FilledRectangle {...}
public final class TransparentRectangle extends Rectangle {...}
public final class FilledRectangle extends Rectangle {...}

public non-sealed class Square extends Shape {...}

You want to permit only the specified classes to extend Shape. Now what's the point in making Square non-sealed? Because you want to allow any other class to extend Square (and the hierarchy).

Think of it like that: Any class that wants to extend Shape will have to do that with either Circle, Rectangle or Square in between. So every extending class of this sub-hierarchy will be either a Circle, Rectangle or a Square (is-a relationship).

The sealed/non-sealed-combination allows you to "seal" only parts of your hierarchy, not all of it (starting from a root).


Notice what the JEP 360 tells us about the permitted classes:

Every permitted subclass must choose a modifier to describe how it continues the sealing initiated by its superclass:

The options are: final, sealed or non-sealed. You are forced to be explicit, so we need non-sealed to "break the seal".


Brian Goetz has posted a realistic use case and explained what the real life benefits are. I want to add another example:

Imagine you are developing a game with heroes and monsters. Some classes could be like that:

public sealed class Character permits Hero, Monster {}

public sealed class Hero extends Character permits Jack, Luci {}
public non-sealed class Monster extends Character {}

public final class Jack extends Hero {}
public final class Luci extends Hero {}

The game has two main characters and there are several enemies. The main characters are set in stone but there can be as many different monsters as you like. Every character in the game is either a hero or a monster.

This is a minimal example, which is hopefully a little more illustrative, and there might be changes, e.g. the addition of a class CustomHero that enables modders to create custom heroes.

akuzminykh
  • 4,522
  • 4
  • 15
  • 36
  • 1
    Wonderful. Now let's make it a rule of thumb to always add one more deriving permits class named "other", in case we change our mind about this silly sealed decision. – Gonen I Sep 12 '20 at 12:44
  • Excellent example, but, since I haven't read JEP 360 myself, please clarify for me: I understand that `final` prevents subclassing, so `sealed` doesn't really apply, but you specified `sealed` on `Rectangle`, which implies that it is not "inherited" from `Shape`. If so, why would you need `non-sealed` on `Square`? Or is `sealed` inherited, and you simply repeated it on `Rectangle` for confirmation, even though it's redundant? – Andreas Sep 12 '20 at 12:47
  • 1
    @GonenI If you do that, you might as well not use `sealed` in the first place. – Andreas Sep 12 '20 at 12:48
  • 1
    @Andreas Not having `non-sealed` on `Square` would be a compiler error. You have to be explicit. – Jorn Vernee Sep 12 '20 at 12:52
  • So subclasses of a `sealed` type must specify one of `final`, `sealed`, or `non-sealed`? Would be good to say that in the answer, as part of why we need the `non-sealed` keyword. – Andreas Sep 12 '20 at 12:56
  • 6
    @Andreas a better real life example: `Throwable` was intended to have only two subclasses, `Error` and `Exception`, forming two fundamental categories. Both classes, `Error` and `Exception` are allowed to have subclasses and should have them, but direct subclasses of `Throwable`, other than those two, were not intended, but unfortunately, could not be forbidden in the past. – Holger Sep 15 '20 at 13:04
1

According to this documentation, the non-sealed class allows opening part of the inheritance hierarchy to the world. It means the root sealed class permits only a closed set of subclasses to extend it.

However, the subclasses can still allow themselves to be extended by any number of subclasses by using the non-sealed keyword.

public sealed class NumberSystem
    // The permits clause has been omitted
    // as all the subclasses exists in the same file.
{ }
final class Binary extends NumberSystem { .. }

final class Octal extends NumberSystem { .. }

final class HexaDecimal extends NumberSystem { .. }

non-sealed class Decimal extends NumberSystem { .. }

final class NonRecurringDecimal extends Decimal {..}
final class RecurringDecimal extends Decimal {..}
Praj
  • 451
  • 2
  • 5
0

Imagin you wanna write a class Shape in your code. You can make this class abstract too if you don't wanna instantiate it. You want to extend it in some classes like Circle , Triangle, and Rectangle. Now that you have implemented it you want to make sure that no one will be able to extend your Shape class. Can you do it without the sealed keyword?
No, because you will have to either make it final in which case you won't be able to extend it to have any subclasses. This is where the sealed keyword comes in! You make an abstract class sealed and restrict that which classes will be able to extend it:

public abstract sealed class Shape 
    permits Circle, Triangle, Rectangle {...}

Remember if your subclasses are not in the same package as Shape class, you will have to mention their name with packages:

public abstract sealed class Shape
    permits com.example.Circle    { ... }

Now, when you are declaring those three subclasses you have to make them final, sealed or non-sealed (One and only one of these modifiers should be used).

Now when you can extend Circle in desired classes? Only when you tell Java it can be extended by unknown subclasses by using non-sealed keyword:

public non-sealed class Circle {...} 
Omid.N
  • 824
  • 9
  • 19
  • 2
    This does not anwer the question. The question is about the `non-sealed` keyword, which is another keyword besides the `sealed` keyword. – Jesper Sep 12 '20 at 12:34
  • 2
    Wow, this feature seems to directly contradict the open closed principle. I envision many cases where you got the permits list wrong, and have to return in the future to open it up again. – Gonen I Sep 12 '20 at 12:34
  • @Jesper I edited my answer! I hope you already got what is the purpose of this new feature! – Omid.N Sep 12 '20 at 13:16