Java 17中的封闭类是什么?

56
今天,我将我的Java版本从16更新到了17,并发现其中有一个新特性是sealed类。我认为可以这样声明:
public sealed class Main permits AClass, AnotherClass {
}

但是,在Java中封闭类有什么用呢?
我也知道这是JDK 15中的一个预览功能。

7
就在几天前,这篇文章发布了,介绍了关于这个话题。它可能会让您对“密封”类有一个概念。 - deHaar
3
改编道格拉斯·亚当斯的话:“在开始时[继承]被[发明]。 这让很多人感到非常愤怒,并被广泛认为是一个糟糕的举动。” - Doug Warren
4
这是一篇更权威的文章:https://www.infoq.com/articles/java-sealed-classes/ - Brian Goetz
1
这里有一个快速的两分钟视频解释:https://youtu.be/SSog4r1IiXU - VedantK
在开始的时候...:一个视频片段(在1分10秒处)。 - undefined
8个回答

45

您可以通过此链接查看示例。

简而言之,密封类使您能够控制哪些模型、类等可以实现或扩展该类/接口。

来自链接的示例:

public sealed interface Service permits Car, Truck {

    int getMaxServiceIntervalInMonths();

    default int getMaxDistanceBetweenServicesInKilometers() {
        return 100000;
    }
}

这个接口只允许 Car 和 Truck 实现它。


16
有一个问题让我感到困惑。为什么限制实现的数量会有用?这样做会给编写单元测试带来麻烦,并且我没有看到真正的优势... - 30thh
5
我不明白这会让编写单元测试变得更加困难,请详细说明一下。 使用sealed类编写代码可以非常清晰地表达类或接口的意图,并且可以帮助避免在大型代码库中出现一些不幸的情况,特别是当有大量开发人员参与时。 - JDTheOne
1
需要解决的一个问题是为密封类创建模拟对象。我认为,清晰的结构是一件好事,但如果它很有用,我们就需要一个特殊的关键字。我们已经有了“final”,其目的非常相似... - 30thh
1
@30thh 相关 - 现在 Kotlin 正在转向 Java 17 字节码(在 Kotlin 1.7 中),我们正在看到这样的情况:https://github.com/mockk/mockk/issues/832#issuecomment-1184975908 - Yair Galler
2
在 Kotlin 中,一种常见的模式是使用 sealed "result class",它具有一组子类,表示不同类型的结果,允许您通过 when 表达式对不同的结果做出反应。我认为这也可以是 Java 中 sealed 接口/类的一个用例。另一个可能的用例是拥有一个通用的 sealed 接口和几个 non-sealed 子接口,因为您的 API 设计要求您只实现子接口,而不是直接实现通用接口。这可能不是正确的 OOP,但这并不意味着某些 API 不会受益。 - Slaw
显示剩余3条评论

31

JEP 409解释道:

密封类或接口只能由允许这样做的那些类和接口来扩展或实现。

更实际的解释如下:

过去的情况是:

  • 不能限制一个接口被另一个接口扩展
  • 不能约束哪些类能够实现特定接口。
  • 必须将类声明为final,以防止其他类扩展它。这样,没有类可以扩展声明为final的类。这是一种非黑即白的方法。

使用sealed关键字的当前情况是:

  • 现在,您可以限制其他接口扩展接口并针对仅允许扩展它的某些特定接口制定规则。

    例如:

    public sealed interface MotherInterface permits ChildInterfacePermitted {}
    
    //Has to be declared either as sealed or non-sealed
    public non-sealed interface ChildInterfacePermitted extends MotherInterface {}  
    
    public interface AnotherChildInterface extends MotherInterface {} 
    //compiler error! It is not included in the permits of mother inteface
    
    现在可以创建一个接口并选择只允许特定的类来实现该接口,其它所有类都不能实现它。 示例:
     public sealed interface MotherInterface permits ImplementationClass1 {} 
    
     //Has to be declared either as final or as sealed or as non-sealed
     public final class ImplementationClass1 implements MotherInterface {} 
    
     public class ImplementationClass2 implements MotherInterface {} 
     //compiler error! It is not included in the permits of mother inteface
    
  • 现在您可以限制一个类被继承(与 final 相同),但现在您可以允许一些特定的类进行继承。因此,现在您比使用关键字 final 更具有控制力,因为关键字 final 是绝对限制每个类都无法继承已声明为 final 的类。

    示例:

  • public sealed class MotherClass permits ChildClass1 {}
    
    //Has to be declared either as final or as sealed or as non-sealed
    public non-sealed class ChildClass1 extends MotherClass {} 
    
     public class ChildClass2 extends MotherClass {} 
     //compiler error! It is not included in the permits of MotherClass
    

重要提示:

  • 封闭类及其允许的子类必须属于同一模块,并且如果声明在一个未命名的模块中,则必须属于同一包。

    例如:

    假设我们有同一个未命名模块和以下几个包:

  -packageA
     -Implementationclass1.java
  -packageB
     -MotherClass.java
或者
   -root
      -MotherClass.java
      -packageA
         -Implementationclass1.java

你会收到错误提示 Class is not allowed to extend sealed class from another package。所以,如果你有一个未命名模块,所有参与封闭函数的类和接口都必须放置在同一个包中。

每个允许的子类必须直接扩展密封类。


4
封闭类是Java语言的一项新增功能,使类作者可以对可以扩展它的类进行细粒度控制。在此之前,您只能允许每个人继承您的类或完全禁止(使用“final”)。它也适用于接口。
此外,封闭类和接口是模式匹配功能的先决条件,因为在编译期间已知所有后代。
像往常一样,缺点是 - 封闭的类和接口无法被模拟/伪造,这会影响测试。

3

密封类

密封类是一种限制,只允许给定的类来实现它。这些允许的类必须明确地扩展密封类,并且还必须有以下其中之一的修饰符:sealednon-sealedfinal。该功能已在Java 17中发布 (JEP 409),并且在此之前的Java 15中作为预览版本提供。

sealed interface IdentificationDocument permits IdCard, Passport, DrivingLicence { }

final class IdCard implements IdentificationDocument { }
final class Passport implements IdentificationDocument { }
non-sealed class DrivingLicence implements IdentificationDocument { }
class InternationalDrivingPermit extends DrivingLicence {}

使用模式匹配

我认为这个新功能和Java 17中引入的模式匹配预览版(JEP 406)一起使用非常棒!

允许的类限制确保所有子类在编译时都是已知的。使用switch表达式(自Java 14以来的JEP 361),编译器要求列出所有允许的类或使用default关键字处理剩余的类。考虑以下使用上述类的示例:

final String code = switch(identificationDocument) {
    case IdCard idCard -> "I";
    case Passport passport -> "P";
};

编译器在执行 javac Application.java --enable-preview -source 17 命令时出现了错误:
Application.java:9: error: the switch expression does not cover all possible input values
                final String code = switch(identificationDocument) {
                                    ^
Note: Application.java uses preview features of Java SE 17.
Note: Recompile with -Xlint:preview for details.
1 error

使用所有允许的类或 default 关键字后,编译成功:

final String code = switch(identificationDocument) {
    case IdCard idCard -> "I";
    case Passport passport -> "P";
    case DrivingLicence drivingLicence -> "D";
};

1
根据这份文档,密封类和接口限制其他类或接口可以扩展或实现它们。这更像是一种声明性的方式来限制超类的使用,而不是使用访问修饰符。
在Java中,一个类可以是final,因此没有其他类可以对其进行子类化。如果一个类不是final,则开放给所有其他类以支持代码可重用性。这样做会引起数据建模方面的担忧。
下面的NumberSystem类对所有类都是开放的,因此任何子类都可以扩展它。如果您想将此NumberSystem限制为一组固定的子类(二进制、十进制、八进制和十六进制)怎么办?这意味着您不希望任何其他任意类扩展此NumberSystem类。
class NumberSystem { ... }
final class Binary extends NumberSystem { ... }
final class Decimal extends NumberSystem { ... }
final class Octal extends NumberSystem { ... }
final class HexaDecimal extends NumberSystem { ... }

使用密封类(sealed class),可以通过控制可以扩展它的子类并防止任何其他随意的类这样做来实现此目的。

0
所有的Java类或接口必须使用permits关键字进行封装。例如:

Parent.class:

public sealed class Parent permits Child1, Child2 {
  void parentMethod() {
    System.out.println("from a sealed parent class ");
  }
}

Child1.java:

public final class Child1 extends Parent {
  public static void main(String[] args) {
    Child1 obj = new Child1();
    obj.parentMethod();
  }
}

Child2.java:

public final class Child2 extends Parent {
  public static void main(String[] args) {
    Child2 obj = new Child2();
    obj.parentMethod();
  }
}

Child3.java

public final class Child3 extends Parent {
  public static void main(String[] args) {
    Child3 obj = new Child3();
    obj.parentMethod();
  }
}

这个 Child3 类的代码会抛出一个编译时错误,说 扩展密封类 Parent 的类型 Child3 应该是 Parent 的允许子类型 (permits Child3,就像 Child1Child2 一样)。


没有使用 permits 关键字的密封类编译时不会出现问题,因此似乎实际上并不需要它。从我的测试中,看起来当省略 permits 关键字时,只有嵌套类才被允许扩展密封类。(我认为这也适用于接口,但我还没有测试过) - Michael Pfaff

0


0

Java中的封闭类是什么?

final修饰符可以被视为一种强制封闭的形式,其中完全禁止扩展/实现。

从概念上讲:final = sealed + 一个空的permits子句。

与完全禁止扩展/实现的final不同,Sealed class/interface限制了哪些其他类或接口可以扩展或实现它们。


历史

  1. 密封类是由JEP 360提出并在JDK 15中作为预览功能发布的。
  2. 它们在JEP 397中再次提出,并在JDK 16中作为预览功能发布,经过改进。
  3. 本JEP提议在JDK 17中最终确定密封类,与JDK 16相比没有任何变化。

目标

  • 允许类或接口的作者控制负责实现它的代码。

  • 提供比访问修饰符更具声明性的方式来限制超类的使用。

描述

类/接口通过在其声明中应用sealed修饰符来封闭。
然后,在任何扩展和实现子句之后,permits子句指定了允许扩展封闭类的类。
例如,下面的Loan声明指定了允许的UnsecuredLoanSecuredLoan子类: sealed interface Loan permits UnsecuredLoan,SecuredLoan{}
final class UnsecuredLoan implements Loan {}
record SecuredLoan() implements Loan{}

封闭类与模式匹配的好处

使用模式匹配,而不是使用if-else链来检查封闭类的实例,我们可以使用增强的switch语句来进行类型测试匹配。

这将允许Java编译器为我们检查所有允许的类是否都被覆盖。

例如,考虑以下代码:

void checkLoanType(Loan loan) {
    if (loan instanceof UnsecuredLoan unsecuredLoan) {
//    something
    } else if (loan instanceof SecuredLoan securedLoan) {
//     something
    }
}

Java编译器无法确保instanceof测试覆盖Loan的所有允许的子类。因此,如果省略了任何instanceof Loan,将不会发出编译时错误消息。
相比之下,使用模式匹配的switch表达式,编译器可以确认覆盖了Loan的每个允许的子类。此外,如果缺少任何一个case,编译器还会发出错误消息。
void checkLoanType(Loan loan) {
     switch (loan) { 
       case SecuredLoan securedLoan -> {} //generated by compiler.
       case UnsecuredLoan unsecuredLoan -> {} //generated by compiler.
     }
}

参考: JEP 360: 封闭类

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接