理解Sealed Classes的诞生背景与核心目标

在面向对象编程中,继承是一把双刃剑。它虽然提供了强大的代码复用和扩展能力,但不受控制的继承关系也可能破坏类的封装性,导致设计意图模糊,并使得代码难以维护。在Java 17之前,类的继承只有两种极端状态:要么被声明为final,完全禁止继承;要么保持开放,允许任何类继承。为了在这两者之间找到一个平衡点,提供一种“有限制的”继承模型,Sealed Classes(密封类)作为一项预览特性在Java 15中被引入,并最终在Java 17中正式成为标准功能。

Sealed Classes语法精讲

Sealed Classes通过一组新的关键字,允许开发者明确指定哪些类或接口可以继承或实现它,从而精确控制继承层次结构。

声明一个密封类

使用sealed关键字声明一个密封类,并通过permits子句列出允许继承它的所有子类。

public sealed class Shape permits Circle, Square, Rectangle {    // 类的属性和方法}

定义密封类的子类

permits子句授权的子类,必须直接继承该密封类。并且,子类必须使用以下三个修饰符之一来明确其继承状态:

  • final: 禁止进一步继承(最终类)。
  • sealed: 允许进一步继承,但必须指定下一级的子类(密封类)。
  • non-sealed: 允许任何类继承(非密封类)。
// 最终子类public final class Circle extends Shape {    public final double radius;    // 构造函数和方法}// 密封子类,可继续控制继承public sealed class Rectangle extends Shape permits FilledRectangle {    public final double length, width;    // 构造函数和方法}public final class FilledRectangle extends Rectangle {    public final String color;    // 构造函数和方法}// 非密封子类,开放继承public non-sealed class Square extends Shape {    public final double side;    // 构造函数和方法}// 任何类现在都可以继承Squarepublic class SpecialSquare extends Square {    // ...}

Sealed Classes的技术优势与设计价值

Sealed Classes的引入为Java开发者带来了显著的技术优势和设计价值。

增强代码的可读性与可维护性

通过permits子句,类的设计意图变得一目了然。任何阅读代码的人都能清晰地知道整个类的继承体系结构,降低了理解和维护大型代码库的认知负担。

实现详尽的编译时检查

switch表达式(同样是Java 17的正式特性)结合使用时,Sealed Classes的能力得到最大发挥。编译器能够判断switch是否已经覆盖了所有可能的子类类型。如果没有覆盖全部情况,编译器会报错,这极大地增强了代码的健壮性,避免了运行时错误。

// 这是一个详尽的switch表达式,编译器知道Shape只有三种可能double area = switch (shape) {    case Circle c -> Math.PI  c.radius  c.radius;    case Square s -> s.side  s.side;    case Rectangle r -> r.length  r.width;    // 无需default子句,因为所有情况已覆盖};

在领域建模中的精确控制

在领域驱动设计(DDD)中,Sealed Classes是建模“和类型”(Sum Type)或“代数数据类型”(ADT)的理想工具。它能够精确地表示一个领域概念只能是一组固定类型中的一种,例如“支付方式”只能是“信用卡”、“ PayPal”或“银行转账”中的一种,这种限制在编译时就能得到保证。

实际应用中的考量与最佳实践

在实际项目中应用Sealed Classes时,应注意以下几点。

模块化系统的交互

如果密封类及其子类位于不同的模块中,必须在module-info.java中通过exports语句将密封类所在的包显式导出给子类所在的模块。这是Java模块系统确保强封装性的一部分。

与Record类的协同使用

Record类(记录类)因其不可变和透明数据的特性,非常适合作为Sealed Classes的叶子节点(即用final修饰的子类)。这两者结合可以简洁而安全地构建复杂的数据模型。

public sealed interface Expr    permits ConstantExpr, PlusExpr, TimesExpr, NegExpr {...}public record ConstantExpr(int i) implements Expr {} //  final recordpublic record PlusExpr(Expr a, Expr b) implements Expr {} // final record// ... 其他记录类

审慎选择子类修饰符

在设计继承链时,应审慎选择子类的修饰符:

  • 如果确定某个分支的扩展已经完成,使用final
  • 如果希望某个分支可以扩展,但需要保留控制权,使用sealed并规划好下一层结构。
  • 如果决定对某个分支完全开放继承(例如,觉得未来可能需要极大的灵活性),使用non-sealed。但要注意,这相当于在该节点放弃了密封性控制。

总结

Java 17的Sealed Classes是一项强大而精密的功能,它填补了类继承控制领域的空白。通过允许开发者明确定义一个类的合法继承者,它不仅提升了代码的清晰度和安全性,还通过与Pattern Matching for switch等特性的结合,为编写更健壮、更易于推理的代码奠定了坚实基础。对于旨在构建严格、可维护且意图明确的领域模型的开发者而言,掌握并应用Sealed Classes是现代Java开发中的一项关键技能。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐