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

2023-12-19

密封课程 and 密封接口 were a Java 15 中的预览功能 https://openjdk.java.net/jeps/360,有一个Java 16 中的第二次预览 https://openjdk.java.net/jeps/397, 现在建议在 Java 17 中交付 https://openjdk.java.net/jeps/409.

他们提供了经典的例子,例如Shape -> Circle, Rectangle, etc.

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

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


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

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

(提前为示例的做作和冗长表示歉意)

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

此时我们遇到了继承的最基本问题之一 - 有很多方法可以“打破封装”。因为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.

这不是世界末日或其他什么,它只是需要考虑和记录的事情 - “如何扩展这个类以及你允许重写什么”,但是当你有一个可供扩展的公共类时anyone可以延长它。

如果您跳过记录如何子类化或记录得不够多,那么很容易陷入这样的情况:您无法控制的使用库或模块的代码可能依赖于您现在所坚持的超类的一个小细节。

密封类可以让您通过打开您的超类来仅扩展您想要的类来避免这种情况。

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

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

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

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

您可能不确定您是否don't稍后想使用继承来节省类之间的一些重复,但由于您的 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 and 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 之前,为了解决这个问题,人们使用了“类型安全访客”模式 https://en.wikipedia.org/wiki/Visitor_pattern。为了您的理智,我建议不要学习它,但如果您好奇,您可以查看代码ANTLR https://en.wikipedia.org/wiki/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;
    }
}

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

“密封接口 + 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;
    }
}

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

封顶

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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Java 中的“密封接口”有什么意义? 的相关文章

  • 缓冲后台输入流实现

    我已经写了背景InputStream and OutputStream 包装其他流并在后台线程上预读的实现 主要允许在处理解压缩流的不同线程中进行解压缩 压缩 这是一个相当标准的生产者 消费者模型 这似乎是一种利用多核 CPU 的简单方法
  • Maven 中的 Java EE 7 First Cup 教程错误:找不到工件 org.glassfish.javaeetutorial.firstcup:firstcup:pom:7.0.1-SNAPSHOT

    我正在关注 FirstCup 的 Java EE 7 更新版本 我安装了 glassfish 4 并使用 NetBeans 7 3 我正在研究第一个示例 但遇到了 Maven 问题 我生成了原型并能够创建 dukes age 项目 然而它有
  • 为什么不同类型的空集合相等?

    下面的机制是什么使不同类型相同 import static org testng Assert assertEquals Test public void whyThisIsEqual assertEquals new HashSet lt
  • Spring boot JPA不使用findById返回现有结果

    我使用 Oracle 数据库和一些 JPA 查询创建了一个非常小且简单的 Spring Boot 应用程序 这是不返回数据的代码片段 该数据实际上存在于数据库中 letterRecipientNonOas letterRecipientNo
  • 时间序列中的峰值检测

    我目前正在开展一个小项目 我想在其中比较两个时间序列 相似性度量确实很模糊 如果两个时间序列大致具有相同的形状 则它们被认为是相似的 所以我心想 如果它们只需要具有相同的形状 我只需比较两个时间序列的峰值 如果峰值位于相同的位置 那么时间序
  • spring-data-mongodb 在重新水化对象时到底如何处理构造函数?

    我读过了http static springsource org spring data data mongo docs 1 1 0 RELEASE reference html mapping chapter http static sp
  • JPA中flush的确切目的是什么

    一些令人困惑的解释 冲洗 刷新是将底层持久存储与内存中保存的持久状态同步的过程 它将更新或插入到正在运行的事务中的表中 但它可能不会提交这些更改 如果无论如何更改仅在提交后才会保留在数据库中 那么为什么要在代码中间刷新呢 运行刷新后 如果对
  • 当 xml 具有名称空间前缀时,为什么只有某些 XPath 表达式才能找到节点

    在下面的示例代码中 任何采用 形式的 XPath元素名称 当源 xml 有命名空间前缀时返回 null 请参阅testWithNS 在底部的代码中 当源 xml 没有名称空间前缀时 所有列出的 XPath 表达式都会返回一个节点 请参阅te
  • Java 中等效的并行扩展

    我在 Net 开发中使用并行扩展有一些经验 但我正在考虑在 Java 中做一些工作 这些工作将受益于易于使用的并行库 JVM 是否提供任何与并行扩展类似的工具 您应该熟悉java util concurrent http java sun
  • Grails 3.x bootRun 失败

    我正在尝试在 grails 3 1 11 中运行一个项目 但出现错误 失败 构建失败并出现异常 什么地方出了错 任务 bootRun 执行失败 进程 命令 C Program Files Java jdk1 8 0 111 bin java
  • Spring Batch 多线程 - 如何使每个线程读取唯一的记录?

    这个问题在很多论坛上都被问过很多次了 但我没有看到适合我的答案 我正在尝试在我的 Spring Batch 实现中实现多线程步骤 有一个包含 100k 条记录的临时表 想要在 10 个线程中处理它 每个线程的提交间隔为 300 因此在任何时
  • 为什么 i++ 不是原子的?

    Why is i Java 中不是原子的 为了更深入地了解 Java 我尝试计算线程中循环的执行频率 所以我用了一个 private static int total 0 在主课中 我有两个线程 主题 1 打印System out prin
  • 如何在 Play java 中创建数据库线程池并使用该池进行数据库查询

    我目前正在使用 play java 并使用默认线程池进行数据库查询 但了解使用数据库线程池进行数据库查询可以使我的系统更加高效 目前我的代码是 import play libs Akka import scala concurrent Ex
  • 制作一个交互式Windows服务

    我希望我的 Java 应用程序成为交互式 Windows 服务 用户登录时具有 GUI 的 Windows 服务 我搜索了这个 我发现这样做的方法是有两个程序 第一个是服务 第二个是 GUI 程序并使它们进行通信 服务将从 GUI 程序获取
  • INSERT..RETURNING 在 JOOQ 中不起作用

    我有一个 MariaDB 数据库 我正在尝试在表中插入一行users 它有一个生成的id我想在插入后得到它 我见过this http www jooq org doc 3 8 manual sql building sql statemen
  • Spark 1.3.1 上的 Apache Phoenix(4.3.1 和 4.4.0-HBase-0.98)ClassNotFoundException

    我正在尝试通过 Spark 连接到 Phoenix 并且在通过 JDBC 驱动程序打开连接时不断收到以下异常 为简洁起见 下面是完整的堆栈跟踪 Caused by java lang ClassNotFoundException org a
  • 反射找不到对象子类型

    我试图通过使用反射来获取包中的所有类 当我使用具体类的代码 本例中为 A 时 它可以工作并打印子类信息 B 扩展 A 因此它打印 B 信息 但是当我将它与对象类一起使用时 它不起作用 我该如何修复它 这段代码的工作原理 Reflection
  • Liferay ClassNotFoundException:DLFileEntryImpl

    在我的 6 1 0 Portal 实例上 带有使用 ServiceBuilder 和 DL Api 的 6 1 0 SDK Portlet 这一行 DynamicQuery query DynamicQueryFactoryUtil for
  • 我可以使用 HSQLDB 进行 junit 测试克隆 mySQL 数据库吗

    我正在开发一个 spring webflow 项目 我想我可以使用 HSQLDB 而不是 mysql 进行 junit 测试吗 如何将我的 mysql 数据库克隆到 HSQLDB 如果您使用 spring 3 1 或更高版本 您可以使用 s
  • 无法解析插件 Java Spring

    我正在使用 IntelliJ IDEA 并且我尝试通过 maven 安装依赖项 但它给了我这些错误 Cannot resolve plugin org apache maven plugins maven clean plugin 3 0

随机推荐