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

Bha*_*ala 9 java interface sealed-class java-15 java-sealed-type

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

他们提供了典型的例子一样Shape- > Circle,Rectangle等等。

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

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

Eth*_*Cue 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();
    }
}
Run Code Online (Sandbox Code Playgroud)

现在,您想要向库中添加一个新版本,该版本将在预订时打印出预订人员的姓名。有几种可能的途径可以做到这一点。

如果您从头开始设计,您可以合理地Airport用Airport接口替换类,并设计类似这样的PrintingAirport组合BasicAirport。

public interface Airport {
    void bookPerson(String name);

    void bookPeople(String... names);

    int peopleBooked();
}
Run Code Online (Sandbox Code Playgroud)
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();
    }
}
Run Code Online (Sandbox Code Playgroud)
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();
    }
}
Run Code Online (Sandbox Code Playgroud)

但这在我们的假设中是不可行的,因为该类Airport已经存在。将会有new Airport()一些特定的类型的调用和方法,Airport除非我们使用继承,否则无法以向后兼容的方式保留这些类型。

final因此,要在 java 15 之前执行此操作,您需要从类中删除并编写子类。

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();
    }
}
Run Code Online (Sandbox Code Playgroud)
public final class PrintingAirport extends Airport {
    @Override
    public void bookPerson(String name) {
        System.out.println(name);
        super.bookPerson(name);
    }
}
Run Code Online (Sandbox Code Playgroud)

此时我们遇到了继承的最基本问题之一 - 有很多方法可以“打破封装”。因为bookPeoplein 中的方法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();
    }
}
Run Code Online (Sandbox Code Playgroud)

那么PrintingAirport子类将无法正确运行,除非它也重写了bookPeople. 进行相反的更改,除非它不覆盖 ,否则它将无法正确运行bookPeople。

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

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

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

public sealed class Airport permits PrintingAirport {
    // ...
}
Run Code Online (Sandbox Code Playgroud)

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

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

public interface Airport {
    // ...
}
Run Code Online (Sandbox Code Playgroud)
public final class BasicAirport implements Airport {
   // ...
}
Run Code Online (Sandbox Code Playgroud)
public final class PrintingAirport implements Airport {
    // ...
}
Run Code Online (Sandbox Code Playgroud)

您可能不确定以后是否不想使用继承来保存类之间的一些重复,但由于您的 Airport 接口是公共的,因此您需要进行一些中间abstract class或类似的操作。

你可以防御性地说“你知道吗,在我更好地了解我想要这个 API 的去向之前,我将成为唯一能够实现该接口的人”。

public sealed interface Airport permits BasicAirport, PrintingAirport {
    // ...
}
Run Code Online (Sandbox Code Playgroud)
public final class BasicAirport implements Airport {
   // ...
}
Run Code Online (Sandbox Code Playgroud)
public final class PrintingAirport implements Airport {
    // ...
}
Run Code Online (Sandbox Code Playgroud)

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

假设您向 Web 服务发送请求,它将返回 JSON 中的两个内容之一。

{
    "color": "red",
    "scaryness": 10,
    "boldness": 5
}
Run Code Online (Sandbox Code Playgroud)
{
    "color": "blue",
    "favorite_god": "Poseidon"
}
Run Code Online (Sandbox Code Playgroud)

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

因为这是 Java,所以我们需要将原始的非类型 JSON 表示映射到类中。我们来模拟一下这种情况。

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

public enum SillyColor {
    RED, BLUE
}
Run Code Online (Sandbox Code Playgroud)
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
}
Run Code Online (Sandbox Code Playgroud)

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

我们至少可以使用color枚举而不是字符串,以便代码不需要处理任何其他颜色,但它仍然远不理想。不同的案件变得越复杂或越多,情况就会变得更糟。

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

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

public interface SillyResponse {
    SillyColor color();
}
Run Code Online (Sandbox Code Playgroud)

现在,这两个子类将具有不同的方法集,并且获取任一方法的代码都可以用来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
}
Run Code Online (Sandbox Code Playgroud)
public final class Blue implements SillyResponse {
    private final String favoriteGod;

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

    // constructor, accessors, toString, equals, hashCode
}
Run Code Online (Sandbox Code Playgroud)

问题是,因为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");
}
Run Code Online (Sandbox Code Playgroud)

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

旁白:在 java 15 之前,为了弥补这个问题,人们使用了“类型安全访问者”模式。为了您的理智,我建议不要学习这一点,但如果您好奇,您可以查看ANTLR生成的代码 - 它都是不同“形状”数据结构的大型层次结构。

密封课程让您说“嘿,这些是唯一重要的情况。”

public sealed interface SillyResponse permits Red, Blue {
    SillyColor color();
}
Run Code Online (Sandbox Code Playgroud)

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

public sealed interface SillyResponse permits Red, Blue {
}
Run Code Online (Sandbox Code Playgroud)

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

public enum Color { Red, Blue }
Run Code Online (Sandbox Code Playgroud)

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

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

    private final String name;

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

    public String name() {
        return this.name;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

3. 强制不变量

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

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

    public String color() {
        return this.color;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

封顶

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


Boh*_*ian 8

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

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

  • 我相信这里的关键可能是“默认”方法,因为它们是所有实现都不可避免地继承的方法? (2认同)