Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the point of a “sealed interface” in Java?

Sealed classes and sealed interfaces were a preview feature in Java 15, with a second preview in Java 16, and now proposed delivery in Java 17.

They have provided classic examples like Shape -> Circle, Rectangle, etc.

I understand sealed classes: the switch statement example provided makes sense to me. But, sealed interfaces are a mystery to me. Any class implementing an interface is forced to provide definitions for them. Interfaces don't compromise the integrity of the implementation because the interface is stateless on its own. Doesn't matter whether I wanted to limit implementation to a few selected classes.

Could you tell me the proper use case of sealed interfaces in Java 15+?

like image 430
Player_Neo Avatar asked Oct 03 '20 19:10

Player_Neo


People also ask

When would you use a sealed interface?

Summary. Sealed classes and interfaces should be used to represent restricted hierarchies. They make it easier to handle each possible value and, as a result, to add new methods using extension functions. Abstract classes leave space for new classes to join this hierarchy.

What is sealed interface Java?

Key Takeaways. The release of Java SE 15 in Sept 2020 will introduce "sealed classes" (JEP 360) as a preview feature. A sealed class is a class or interface which restricts which other classes or interfaces may extend it.

What is the use of sealed class in Java?

By sealing a class, you can specify which classes are permitted to extend it and prevent any other arbitrary class from doing so. To seal a class, add the sealed modifier to its declaration. Then, after any extends and implements clauses, add the permits clause.

What is the point of sealed classes?

A sealed class, in C#, is a class that cannot be inherited by any class but can be instantiated. The design intent of a sealed class is to indicate that the class is specialized and there is no need to extend it to provide any additional functionality through inheritance to override its behavior.

How to seal an interface in Java?

Like sealed classes, to seal an interface, add the sealed modifier to its declaration. Then, after any extends clause, add the permits clause, which specifies the classes that can implement the sealed interface and the interfaces that can extend the sealed interface. The following example declares a sealed interface named Expr.

What is a sealed class in Java?

A sealed class is a class or interface that restricts which other classes or interfaces may extend. 1. Sealed Classes The sealing of a class or interface can be done using the modifier sealed in the declaration time.

What is the difference between sealed class and sealed interface?

Sealed Interfaces There are not many differences between how a sealed class is defined vs. how a sealed interface is defined. Like sealed classes, to seal an interface, add the sealed modifier to its declaration.

What is the use of issealed in Java?

Sealed classes are also supported by the reflection API, where two public methods have been added to the java.lang.Class: The isSealed method returns true if the given class or interface is sealed. Method permittedSubclasses returns an array of objects representing all the permitted subclasses.


Video Answer


3 Answers

Although interfaces have no state themselves, they have access to state, eg via getters, and may have code that does something with that state via default methods.

Therefore the reasoning supporting sealed for classes may also be applied to interfaces.

like image 117
Bohemian Avatar answered Oct 21 '22 22:10

Bohemian


Basically to give a sealed hierarchy when there is no concrete state to share across the different members. That's the major difference between implementing an interface and extending a class - interfaces don't have fields or constructors of their own.

But in a way, that isn't the important question. The real issue is why you would want a sealed hierarchy to begin with. Once that is established it should be clearer where sealed interfaces fit in.

(apologies in advance for the contrived-ness of examples and the long winded-ness)

1. To use subclassing without "designing for subclassing".

Lets say you have a class like this, and it is in a library you already published.

public final class Airport {
    private List<String> peopleBooked;

    public Airport() {
        this.peopleBooked = new ArrayList<>();
    }

    public void bookPerson(String name) {
        this.peopleBooked.add(name);
    }

    public void bookPeople(String... names) {
        for (String name : names) {
            this.bookPerson(name);
        }
    }

    public int peopleBooked() {
        return this.peopleBooked.size();
    }
}

Now, you want to add a new version to your library that will print out the names of people booked as they are booked. There are several possible paths to do this.

If you were designing from scratch, you could reasonably replace the Airport class with an Airport interface and design the PrintingAirport to compose with a BasicAirport like so.

public interface Airport {
    void bookPerson(String name);

    void bookPeople(String... names);

    int peopleBooked();
}
public final class BasicAirport implements Airport {
    private final List<String> peopleBooked;

    public Airport() {
        this.peopleBooked = new ArrayList<>();
    }

    @Override
    public void bookPerson(String name) {
        this.peopleBooked.add(name);
    }

    @Override
    public void bookPeople(String... names) {
        for (String name : names) {
            this.bookPerson(name);
        }
    }

    @Override
    public int peopleBooked() {
        return this.peopleBooked.size();
    }
}
public final class PrintingAirport implements Airport {
    private final Airport delegateTo;

    public PrintingAirport(Airport delegateTo) {
        this.delegateTo = delegateTo;
    }

    @Override
    public void bookPerson(String name) {
        System.out.println(name);
        this.delegateTo.bookPerson(name);
    }

    @Override
    public void bookPeople(String... names) {
        for (String name : names) {
            System.out.println(name);
        }

        this.delegateTo.bookPeople(names);
    }

    @Override
    public int peopleBooked() {
        return this.peopleBooked.size();
    }
}

This isn't doable in our hypothetical though because the Airport class already exists. There are going to be calls to new Airport() and methods that expect something of type Airport specifically that can't be kept in a backwards compatible way unless we use inheritance.

So to do that pre-java 15 you would remove the final from your class and write the subclass.

public class Airport {
    private List<String> peopleBooked;

    public Airport() {
        this.peopleBooked = new ArrayList<>();
    }

    public void bookPerson(String name) {
        this.peopleBooked.add(name);
    }

    public void bookPeople(String... names) {
        for (String name : names) {
            this.bookPerson(name);
        }
    }

    public int peopleBooked() {
        return this.peopleBooked.size();
    }
}
public final class PrintingAirport extends Airport {
    @Override
    public void bookPerson(String name) {
        System.out.println(name);
        super.bookPerson(name);
    }
}

At which point we run into one of the most basic issues with inheritance - there are tons of ways to "break encapsulation". Because the bookPeople method in Airport happens to call this.bookPerson internally, our PrintingAirport class works as designed, because its new bookPerson method will end up being called once for every person.

But if the Airport class were changed to this,

public class Airport {
    private List<String> peopleBooked;

    public Airport() {
        this.peopleBooked = new ArrayList<>();
    }

    public void bookPerson(String name) {
        this.peopleBooked.add(name);
    }

    public void bookPeople(String... names) {
        for (String name : names) {
            this.peopleBooked.add(name);
        }
    }

    public int peopleBooked() {
        return this.peopleBooked.size();
    }
}

then the PrintingAirport subclass won't behave correctly unless it also overrided bookPeople. Make the reverse change and it won't behave correctly unless it didn't override bookPeople.

This isn't the end of the world or anything, its just something that needs to be considered and documented - "how do you extend this class and what are you allowed to override", but when you have a public class open to extension anyone can extend it.

If you skip documenting how to subclass or don't document enough its easy to end up in a situation where code you don't control that uses your library or module can depend on a small detail of a superclass that you are now stuck with.

Sealed classes let you side step this by opening your superclass up to extension only for the classes you want to.

public sealed class Airport permits PrintingAirport {
    // ...
}

And now you don't need to document anything to outside consumers, just yourself.

So how do interfaces fit in to this? Well, lets say you did think ahead and you have the system where you are adding features via composition.

public interface Airport {
    // ...
}
public final class BasicAirport implements Airport {
   // ...
}
public final class PrintingAirport implements Airport {
    // ...
}

You might not be sure that you don't want to use inheritance later to save some duplication between the classes, but because your Airport interface is public you would need to make some intermediate abstract class or something similar.

You can be defensive and say "you know what, until I have a better idea of where I want this API to go I am going to be the only one able to make implementations of the interface".

public sealed interface Airport permits BasicAirport, PrintingAirport {
    // ...
}
public final class BasicAirport implements Airport {
   // ...
}
public final class PrintingAirport implements Airport {
    // ...
}

2. To represent data "cases" that have different shapes.

Lets say you send a request to a web service and it is going to return one of two things in JSON.

{
    "color": "red",
    "scaryness": 10,
    "boldness": 5
}
{
    "color": "blue",
    "favorite_god": "Poseidon"
}

Somewhat contrived, sure, but you can easily imagine a "type" field or similar that distinguishes what other fields will be present.

Because this is Java, we are going to want to map the raw untyped JSON representation into classes. Lets play out this situation.

One way is to have one class that contains all the possible fields and just have some be null depending.

public enum SillyColor {
    RED, BLUE
}
public final class SillyResponse {
    private final SillyColor color;
    private final Integer scaryness;
    private final Integer boldness;
    private final String favoriteGod;

    private SillyResponse(
        SillyColor color,
        Integer scaryness,
        Integer boldness,
        String favoriteGod
    ) {
        this.color = color;
        this.scaryness = scaryness;
        this.boldness = boldness;
        this.favoriteGod = favoriteGod;
    }

    public static SillyResponse red(int scaryness, int boldness) {
        return new SillyResponse(SillyColor.RED, scaryness, boldness, null);
    }

    public static SillyResponse blue(String favoriteGod) {
        return new SillyResponse(SillyColor.BLUE, null, null, favoriteGod);
    }

    // accessors, toString, equals, hashCode
}

While this technically works in that it does contain all the data, there isn't all that much gained in terms of type-level safety. Any code that gets a SillyResponse needs to know to check the color itself before accessing any other properties of the object and it needs to know which ones are safe to get.

We can at least make the color an enum instead of a string so that code shouldn't need to handle any other colors, but its still far less than ideal. It gets even worse the more complicated or more numerous the different cases become.

What we ideally want to do is have some common supertype to all the cases that you can switch on.

Because its no longer going to be needed to switch on, the color property won't be strictly necessary but depending on personal taste you can keep that as something accessible on the interface.

public interface SillyResponse {
    SillyColor color();
}

Now the two subclasses will have different sets of methods, and code that gets either one can use instanceof to figure out which they have.

public final class Red implements SillyResponse {
    private final int scaryness;
    private final int boldness;

    @Override
    public SillyColor color() {
        return SillyColor.RED;
    }

    // constructor, accessors, toString, equals, hashCode
}
public final class Blue implements SillyResponse {
    private final String favoriteGod;

    @Override
    public SillyColor color() {
        return SillyColor.BLUE;
    }

    // constructor, accessors, toString, equals, hashCode
}

The issue is that, because SillyResponse is a public interface, anyone can implement it and Red and Blue aren't necessarily the only subclasses that can exist.

if (resp instanceof Red) {
    // ... access things only on red ...
}
else if (resp instanceof Blue) {
    // ... access things only on blue ...
}
else {
    throw new RuntimeException("oh no");
}

Which means this "oh no" case can always happen.

An aside: Before java 15 to remedy this people used the "type safe visitor" pattern. I recommend not learning that for your sanity, but if you are curious you can look at code ANTLR generates - its all a large hierarchy of differently "shaped" data structures.

Sealed classes let you say "hey, these are the only cases that matter."

public sealed interface SillyResponse permits Red, Blue {
    SillyColor color();
}

And even if the cases share zero methods, the interface can function just as well as a "marker type", and still give you a type to write when you expect one of the cases.

public sealed interface SillyResponse permits Red, Blue {
}

At which point you might start to see the resemblance to enums.

public enum Color { Red, Blue }

enums say "these two instances are the only two possibilities." They can have some methods and fields to them.

public enum Color { 
    Red("red"), 
    Blue("blue");

    private final String name;

    private Color(String name) {
        this.name = name;
    }

    public String name() {
        return this.name;
    }
}

But all instances need to have the same methods and the same fields and those values need to be constants. In a sealed hierarchy you get the same "these are the only two cases" guarantee, but the different cases can have non-constant data and different data from each other - if that makes sense.

The whole pattern of "sealed interface + 2 or more record classes" is fairly close to what is intended by constructs like rust's enums.

This also applies equally to general objects that have different "shapes" of behaviors, but they don't get their own bullet point.

3. To force an invariant

There are some invariants, like immutability, that are impossible to guarantee if you allow subclasses.

// All apples should be immutable!
public interface Apple {
    String color();
}
public class GrannySmith implements Apple {
    public String color; // granny, no!

    public String color() {
        return this.color;
    }
}

And those invariants might be relied upon later on in the code, like when giving an object to another thread or similar. Making the hierarchy sealed means you can document and guarantee stronger invariants than if you allowed arbitrary subclassing.

To cap off

Sealed interfaces more or less serve the same purpose as sealed classes, you just only use concrete inheritance when you want to share implementation between classes that goes beyond what something like default methods can give.

like image 44
Ethan McCue Avatar answered Oct 21 '22 22:10

Ethan McCue


Suppose you write an authentication library, containing an interface for password encoding, ie char[] encryptPassword(char[] pw). Your library provides a couple of implementations the user can choose from.

You don't want him to be able to pass in his own implementation that might be insecure.

like image 34
daniu Avatar answered Oct 22 '22 00:10

daniu