Java中“密封接口”的意义是什么? API注意:

问题描述

随着Java 15最近推出,我正在阅读有关它支持的新功能和API的信息。 Java 15具有“密封类”和“密封接口”(JEP 360: Sealed Classes (Preview))作为预览功能

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

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

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

解决方法

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

因此支持function matchBrackets(s) { let x = ""; let c = []; let ok = true; for (let i = 0; i < s.length; i++) { x = s.substr(i,1); switch (x) { case "{": c.unshift("}"); break; case "[": c.unshift("]"); break; case "(": c.unshift(")"); break; case "}": if (c[0] == "}") { c.shift(); } else { ok = false; } break; case "]": if (c[0] == "]") { c.shift(); } else { ok = false; } break; case ")": if (c[0] == ")") { c.shift(); } else { ok = false; } break; } if (!ok) { break; } } if (c.length > 0) { ok = false; } return ok; } let tocheck = []; tocheck.push("a(b][}c"); tocheck.push("a(bc"); tocheck.push("a)(bc"); tocheck.push("a(b)[c]{(d)}"); tocheck.push("(a)"); tocheck.push("((a))"); tocheck.push("(a(b))"); tocheck.push("(b)"); tocheck.push("(b"); tocheck.push("))a(("); for (let i = 0; i < tocheck.length; i++) { console.log(tocheck[i] + ": " + matchBrackets(tocheck[i])); }的类的推理也可以应用于接口。

,

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

我写了一些实验代码和一个辅助blog来说明如何使用密封的接口来为Java提供{em> contractual ,来实现ImmutableCollection接口层次结构结构可验证不变性。我认为这可能是密封接口的实际用例。

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

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

,

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

您不希望他能够通过他自己的可能不安全的实现方式。

,

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

但是从某种意义上说,这不是重要的问题。真正的问题是,为什么要开始使用密封的层次结构。一旦建立,应该更加清楚密封接口的适合位置。

(事先为示例的虚伪和冗长的歉意)

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组成,如下所示。 / p>

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,否则它将无法正常工作。

这不是世界末日或任何事情,这只是需要考虑和记录的东西-“您如何扩展此类以及允许您覆盖的内容”,但是当您打开公开课程时扩展任何人都可以扩展它。

如果您跳过有关如何继承子类的文档或没有提供足够的文档,那么很容易在无法控制的使用库或模块的代码取决于您现在的超类的细节的情况下结束坚持下去。

封闭类允许您通过将超类仅扩展到想要扩展的类来避免这一点。

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

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

那么接口如何适应呢?好吧,可以说您确实有超前的想法,并且您已经有了通过合成来添加要素的系统。

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

您可能不确定自己不想以后再使用继承来保存类之间的某些重复项,但是由于您的Airport接口是公共的,因此您需要进行一些中间{{1 }}或类似的东西。

您可以采取防御性的态度,并说:“您知道什么,直到我对我希望该API的应用有了更好的了解,我将成为唯一能够实现该接口的人。”

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

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

让我们说您向Web服务发送了一个请求,它将返回JSON中的两件事之一。

public final class PrintingAirport implements Airport {
    // ...
}
{
    "color": "red","scaryness": 10,"boldness": 5
}

当然可以,但是您可以轻松地想象一个“类型”字段或类似字段,以区别于其他字段。

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

一种方法是让一个类包含所有可能的字段,而其中一些则{ "color": "blue","favorite_god": "Poseidon" } 依赖。

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,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,hashCode
}

问题在于,因为public final class Blue implements SillyResponse { private final String favoriteGod; @Override public SillyColor color() { return SillyColor.BLUE; } // constructor,hashCode } 是公共接口,所以任何人都可以实现它,并且SillyResponseRed不一定是唯一可以存在的子类。

Blue

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

顺便说一句:在Java 15之前,这个人使用"type safe visitor" pattern。我建议您不要出于理智而学习,但是如果您好奇,可以看一下ANTLR生成的代码-它是由不同形状的数据结构组成的大型层次结构。

密封类可以让您说“嘿,这是唯一重要的情况。”

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

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

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

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

public sealed interface SillyResponse permits Red,Blue {
}
枚举说:“这两个实例是仅有的两种可能性。”他们可以使用一些方法和字段。
public enum Color { Red,Blue }

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

“密封接口+ 2个或更多记录类”的整个模式与rust枚举之类的构造所预期的相当接近。

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

3。强制不变

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

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

    private final String name;

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

    public String name() {
        return this.name;
    }
}
// All apples should be immutable!
public interface Apple {
    String color();
}

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

要封锁

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

,

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

,

接口并不总是完全由其API单独定义。以ProtocolFamily为例。考虑到接口的方法,该接口将易于实现,但结果对预期的语义将无用,因为在最佳情况下,all methods accepting ProtocolFamily as input只会抛出UnsupportedOperationException

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

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

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

API注意:

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

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

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