在Java中,“sealed interface”的用途是什么?

huangapple go评论150阅读模式
英文:

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

问题

密封类密封接口 是 Java 15 中的一个预览功能,在 Java 16 中进行了第二次预览,现在在 Java 17 中提议正式发布

它们提供了经典的示例,比如 Shape -> CircleRectangle 等等。

我理解密封 :提供的 switch 语句示例对我来说是有意义的。但是,密封 接口 对我来说是个谜。实现接口的任何类都被强制为其提供定义。接口本身不会损害实现的完整性,因为接口本身是无状态的。无论我是否想要将实现限制在几个选定的类中,都不重要。

你能告诉我在 Java 15+ 中密封接口的正确用法吗?

英文:

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+?

答案1

得分: 11

基本上,当没有具体状态可在不同成员之间共享时,提供封闭的层次结构是很有必要的。这就是在实现接口和扩展类之间的主要区别 - 接口没有自己的字段或构造函数。

但是,实际上,这并不是最重要的问题。真正的问题是为什么你想要一个封闭的层次结构。一旦确定了这一点,封闭的接口在哪里适用就会更清楚。

(提前为示例和冗长的描述道歉)

1. 无需“为子类化而设计”即可使用子类化

假设你有一个像这样的类,并且它已经在你发布的库中。

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();
    }
}

现在,你想要向你的库添加一个新版本,该版本在预订人员时会打印出被预订的人员的姓名。有几种可能的方法可以做到这一点。

如果你从头开始设计,你可以合理地将Airport类替换为Airport接口,并设计PrintingAirport以与BasicAirport进行组合,如下所示。

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();
    }
}

然而,在我们的假设情况下,这是不可行的,因为Airport类已经存在。会有对new Airport()的调用和对期望某种类型为Airport的方法,除非我们使用继承,否则无法保持向后兼容性。

因此,在 Java 15 之前,你需要从类中移除final修饰符,并编写子类。

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);
    }
}

在这一点上,我们遇到了继承的一个最基本的问题之一 - 有许多方法可以“破坏封装”。由于Airport中的bookPeople方法恰好在内部调用了this.bookPerson,所以我们的PrintingAirport类可以正常工作,因为它的新bookPerson方法将为每个人员调用一次。

但是,如果Airport类更改为以下内容,

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();
    }
}

那么,除非PrintingAirport也覆盖了bookPeople方法,否则它将无法正确地工作。进行相反的更改,除非它没有覆盖bookPeople,否则它将无法正确地工作。

这并不是世界末日或什么,只是需要考虑和记录的一些事情 - “你如何扩展这个类以及你可以覆盖什么”,但是当你的公共类对扩展是开放的时候,任何人都可以对其进行扩展。

如果你跳过了关于如何子类化的文档,或者没有提供足够的文档,很容易陷入这样一种情况:使用你的库或模块的代码可能依赖于你现在无法改变的超类的细小细节。

封闭类通过只为你希望的类打开超类的扩展,让你能够避免这种情况。

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

现在,你不需要为外部消费者编写任何文档,只需为自己编写即可。

那么接口如何适应这一点呢?嗯,假设你已经预先考虑过,并且你正在通过组合来添加功能。

英文:

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&lt;String&gt; peopleBooked;

    public Airport() {
        this.peopleBooked = new ArrayList&lt;&gt;();
    }

    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&lt;String&gt; peopleBooked;

    public Airport() {
        this.peopleBooked = new ArrayList&lt;&gt;();
    }

    @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&lt;String&gt; peopleBooked;

    public Airport() {
        this.peopleBooked = new ArrayList&lt;&gt;();
    }

    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&lt;String&gt; peopleBooked;

    public Airport() {
        this.peopleBooked = new ArrayList&lt;&gt;();
    }

    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.

{
    &quot;color&quot;: &quot;red&quot;,
    &quot;scaryness&quot;: 10,
    &quot;boldness&quot;: 5
}
{
    &quot;color&quot;: &quot;blue&quot;,
    &quot;favorite_god&quot;: &quot;Poseidon&quot;
}

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(&quot;oh no&quot;);
}

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(&quot;red&quot;), 
    Blue(&quot;blue&quot;);

    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.

答案2

得分: 9

尽管接口本身没有状态,但它们可以通过getter访问状态,并且可能具有通过“default”方法对该状态执行操作的代码。

因此,支持将“sealed”用于类的推理也可以应用于接口。

英文:

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.

答案3

得分: 4

假设您编写了一个身份验证库,其中包含用于密码编码的接口,即 char[] encryptPassword(char[] pw)。您的库提供了一些用户可以选择的实现。

您不希望用户能够传入可能不安全的自己实现的内容。

英文:

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.

答案4

得分: 4

你能告诉我在 Java 15+ 中 sealed interfaces 的适用场景吗?

我编写了一些实验性代码,并撰写了一篇配套的博客,以说明如何使用 sealed interfaces 在 Java 中实现 ImmutableCollection 接口层次结构,从而提供合约结构可验证的不可变性。我认为这可能是 sealed interfaces 的一个实际用例。

这个示例包括四个 sealed 接口:ImmutableCollectionImmutableSetImmutableListImmutableBagImmutableCollectionImmutableList/Set/Bag 继承。每个叶子接口都permits两个最终的具体实现。这篇博客描述了限制接口的设计目标,以便开发人员无法实现“不可变”接口并提供可变的实现。

注:我是Eclipse Collections的贡献者。

英文:

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

I wrote some experimental code and a supporting blog to illustrate how sealed interfaces could be used to implement an ImmutableCollection interface hierarchy for Java that provides contractual, structural and verifiable immutability. I think this could be a practical use case for sealed interfaces.

The example includes four sealed interfaces: ImmutableCollection, ImmutableSet, ImmutableList and ImmutableBag. ImmutableCollection is extended by ImmutableList/Set/Bag. Each of the leaf interfaces permits two final concrete implementations. This blog describes the design goal of restricting the interfaces so developers cannot implement "Immutable" interfaces and provide implementations that are mutable.

Note: I am a committer for Eclipse Collections.

答案5

得分: 2

自从 Java 在 14 版本中引入了 records,密封接口的一个用例肯定是创建密封记录。这在密封类的情况下是不可能的,因为记录不能扩展类(就像枚举一样)。

英文:

Since Java introduced records in version 14, one use case for sealed interfaces will certainly be to create sealed records. This is not possible with sealed classes, because records cannot extend a class (much like enums).

答案6

得分: 2

接口并不总是仅由其 API 定义。以 ProtocolFamily 为例。考虑到其方法,这个接口很容易实现,但是由于所期望的语义,使用 ProtocolFamily 作为输入的 所有接受 ProtocolFamily 作为输入的方法 最终只会抛出 UnsupportedOperationException,在最好的情况下。

这是一个典型的例子,如果在早期版本中存在这个功能,这个接口将被封闭;这个接口的目的是抽象库导出的实现,而不是在该库之外拥有实现。

更新的类型 ConstantDesc 甚至明确提到了这个意图:

> 非平台类不应直接实现 ConstantDesc。相反,它们应该扩展 DynamicConstantDesc……

> ### API 注意:
> 在将来,如果 Java 语言允许,ConstantDesc 可能会成为一个封闭的接口,只允许被显式允许的类型进行子类化。

关于可能的用例,在封闭的抽象类和封闭的接口之间没有区别,但是封闭的接口仍然允许实现者扩展不同的类(在作者设置的限制内)。或者被 enum 类型实现。

简而言之,有时候,接口用于在库和客户端之间实现最小的耦合,而不打算在客户端实现它。

英文:

Interfaces are not always entirely defined by their API alone. Take, for example ProtocolFamily. This interface would be easy to implement, considering its methods, but the result would not be useful regarding the intended semantics, as all methods accepting ProtocolFamily as input would just throw UnsupportedOperationException, in the best case.

This is a typical example for an interface that would be sealed if that feature existed in earlier versions; the interface is intended to abstract the implementations exported by a library, but not to have implementations outside that library.

The newer type ConstantDesc mentions that intention even explicitly:

> Non-platform classes should not implement ConstantDesc directly. Instead, they should extend DynamicConstantDesc

> ### API Note:
> In the future, if the Java language permits, ConstantDesc may become a sealed interface, which would prohibit subclassing except by explicitly permitted types.

Regarding possible use cases, there is no difference between a sealed abstract class and a sealed interface, but the sealed interface still allows implementors extending different classes (within the limits set by the author). Or being implemented by enum types.

In short, sometimes, interfaces are used to have the least coupling between a library and its clients, without the intention of having client-side implementations of it.

答案7

得分: 0

在 Java 15 之前,开发者通常认为代码的可重用性是目标。但这并不是完全正确的,在某些情况下,我们希望实现广泛的可访问性,但并不需要可扩展性,以获得更好的安全性和代码库管理。

这个功能旨在在 Java 中实现更精细的继承控制。封闭允许类和接口定义其允许的子类型。

封闭接口允许我们明确地确定可以实现它的所有类。

在Java中,“sealed interface”的用途是什么?

英文:

在Java中,“sealed interface”的用途是什么?

Before java 15 developers used to think in a way that code reusability is the goal. But it's not true to all extents, in some cases we want wide accessibility but not extensibility for better security and also codebase management.

This feature is about enabling more fine-grained inheritance control in Java. Sealing allows classes and interfaces to define their permitted subtypes.

The sealed interface allows us to enable it to reason clearly all the classes that can implement it.

huangapple
  • 本文由 发表于 2020年10月4日 03:16:31
  • 转载请务必保留本文链接:https://go.coder-hub.com/64188082.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定