繁体   English   中英

Java 中的“密封接口”有什么意义?

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

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

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

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

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

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

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

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

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

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

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

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

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

(为示例的人为性和冗长的内容提前道歉)

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()和方法的调用, 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);
    }
}

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

你可能不相信你不想使用继承后保存类之间有一些重复,而是因为你的机场界面公共你需要做一些中间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
}

虽然这在技术上有效,因为它确实包含所有数据,但在类型级安全性方面并没有获得太多收益。 任何获得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是一个公共接口,任何人都可以实现它,而RedBlue不一定是唯一可以存在的子类。

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

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

关闭

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

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

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

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

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

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

非平台类不应直接实现ConstantDesc 相反,他们应该扩展DynamicConstantDesc ...

API注意事项:

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

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

简而言之,有时,接口被用来使库与其客户端之间的耦合最小,而无意在客户端实现它。

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

在此处输入图片说明

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

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM