16

密封类密封接口是 Java 15 中的一个预览功能,Java 16中有第二个预览,现在建议在 Java 17 中交付

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

我了解密封switch提供的语句示例对我来说很有意义。但是,密封接口对我来说是个谜。任何实现接口的类都被迫为它们提供定义。接口不会损害实现的完整性,因为接口本身是无状态的。我是否想将实现限制为几个选定的类都没关系。

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

4

7 回答 7

8

尽管接口本身没有状态,但它们可以访问状态,例如通过 getter,并且可能具有通过default方法对状态进行处理的代码。

因此对类的推理支持sealed也可以应用于接口。

于 2020-10-03T19:25:19.157 回答
8

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

但在某种程度上,这不是重要的问题。真正的问题是为什么你会想要一个密封的层次结构开始。一旦确定了,应该更清楚密封接口的位置。

(提前为示例的做作和冗长的内容道歉)

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类,并设计一个类似这样的组合。AirportPrintingAirportBasicAirport

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

在这一点上,我们遇到了最基本的继承问题之一——有很多方法可以“打破封装”。因为 in 中的bookPeople方法Airport恰好在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 {
    // ...
}

现在,您无需向外部消费者记录任何内容,只需向您自己记录即可。

那么接口如何适应这一点呢?好吧,假设您确实提前考虑过,并且您拥有通过合成添加功能的系统。

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

您可能不确定以后不想使用继承来保存类之间的一些重复,但是因为您的 Airport 接口是公共的,所以您需要做一些中间abstract class或类似的东西。

你可以防御性地说“你知道吗,直到我更好地了解我希望这个 API 去哪里,我将成为唯一能够实现接口的人”。

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

2. 表示具有不同形状的数据“案例”。

假设您向 Web 服务发送请求,它将以 JSON 格式返回两件事之一。

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

当然,有些做作,但是您可以轻松地想象一个“类型”字段或类似字段来区分将出现的其他字段。

因为这是 Java,我们想要将原始的无类型 JSON 表示映射到类中。让我们发挥这种情况。

一种方法是让一个类包含所有可能的字段,并且只有一些null依赖。

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
}

虽然这在技术上是可行的,因为它确实包含所有数据,但在类型级安全性方面并没有太大的收获。任何获得 a 的代码都SillyResponse需要知道color在访问对象的任何其他属性之前检查自身,并且它需要知道哪些是安全的。

我们至少可以制作color一个枚举而不是字符串,这样代码就不需要处理任何其他颜色,但它仍然远不理想。不同的案例变得越复杂或数量越多,情况就会变得更糟。

我们理想情况下想要做的是为所有可以打开的案例提供一些通用的超类型。

因为不再需要打开它,所以该color属性不是绝对必要的,但根据个人喜好,您可以将其保留为界面上可访问的内容。

public interface SillyResponse {
    SillyColor color();
}

现在这两个子类将具有不同的方法集,并且获得其中任何一个的代码可以instanceof用来确定它们具有哪些。

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
}

问题是,因为SillyResponse是一个公共接口,任何人都可以实现它,Red而且Blue不一定是唯一可以存在的子类。

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

这意味着这种“哦不”的情况总是会发生。

顺便说一句:在 java 15 之前,人们使用“类型安全访问者”模式来解决这个问题。我建议不要为了你的理智而学习,但如果你好奇,你可以看看ANTLR生成的代码——它都是一个不同“形状”数据结构的大层次结构。

密封类让你说“嘿,这些是唯一重要的案例”。

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

即使案例共享零个方法,接口也可以像“标记类型”一样发挥作用,并且在您期望其中一个案例时仍然给您一个类型来编写。

public sealed interface SillyResponse permits Red, Blue {
}

此时您可能会开始看到与枚举的相似之处。

public enum Color { Red, Blue }

枚举说“这两个实例是唯一的两种可能性。” 他们可以有一些方法和领域。

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

    private final String name;

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

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

但是所有实例都需要具有相同的方法和相同的字段,并且这些值需要是常量。在密封的层次结构中,您会得到相同的“这是仅有的两种情况”保证,但不同的情况下可以有非常量数据和彼此不同的数据——如果这有意义的话。

“密封接口 + 2 个或更多记录类”的整个模式非常接近 rust 枚举等构造的意图。

这同样适用于具有不同行为“形状”的一般对象,但它们没有自己的要点。

3.强制一个不变量

如果您允许子类,则无法保证某些不变量,例如不变性。

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

并且这些不变量可能会在稍后的代码中被依赖,例如将对象提供给另一个线程或类似线程时。使层次结构密封意味着您可以记录和保证比允许任意子类化更强的不变量。

封顶

密封接口或多或少与密封类具有相同的用途,当您希望在类之间共享超出默认方法所能提供的实现时,您只需使用具体继承即可。

于 2020-11-14T05:57:29.237 回答
3

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

我写了一些实验代码和一个支持博客来说明如何使用密封接口来实现ImmutableCollectionJava 的接口层次结构,该层次结构提供契约性、结构性可验证的不变性。我认为这可能是密封接口的实际用例。

该示例包括四个sealed接口:ImmutableCollectionImmutableSet和。由 扩展。每个叶子接口都有两个最终的具体实现。该博客描述了限制接口的设计目标,因此开发人员无法实现“不可变”接口并提供可变的实现。ImmutableListImmutableBagImmutableCollectionImmutableList/Set/Bagpermits

注意:我是Eclipse Collections的提交者。

于 2020-10-16T05:53:19.680 回答
2

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

您不希望他能够传递他自己的可能不安全的实现。

于 2020-10-04T06:29:06.003 回答
2

接口并不总是完全由它们的 API 单独定义。举个例子ProtocolFamily。考虑到它的方法,这个接口很容易实现,但结果对于预期的语义没有用,因为在最好的情况下,所有接受ProtocolFamily输入的方法都只会 throw 。UnsupportedOperationException

这是一个典型的接口示例,如果该功能存在于早期版本中,该接口将被密封;该接口旨在抽象由库导出的实现,但不具有该库之外的实现。

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

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

API 注释:

将来,如果 Java 语言允许,ConstantDesc它可能会成为一个密封接口,它将禁止子类化,除非是明确允许的类型。

关于可能的用例,密封抽象类和密封接口没有区别,但密封接口仍然允许实现者扩展不同的类(在作者设定的范围内)。或按enum类型实现。

简而言之,有时,接口用于在库与其客户端之间具有最少的耦合,而不是在客户端实现它。

于 2020-10-05T08:57:15.847 回答
1

由于 Java 在版本 14 中引入了记录,密封接口的一个用例肯定是创建密封记录。这对于密封类是不可能的,因为记录不能扩展类(很像枚举)。

于 2020-10-04T13:42:12.550 回答
0

在此处输入图像描述

在 Java 15 之前,开发人员曾经认为代码可重用性是目标。但这并非在所有程度上都是正确的,在某些情况下,我们想要广泛的可访问性而不是可扩展性以获得更好的安全性和代码库管理。

这个特性是关于在 Java 中启用更细粒度的继承控制。密封允许类和接口定义它们允许的子类型。

密封接口允许我们使它能够清楚地推理所有可以实现它的类。

于 2022-01-05T08:13:59.787 回答