Java 17 新特性之密封类型

工作中所有项目都已升级到了当前的 LTS 版 Java 21, 眼看 Java 快要来到了下一个 LTS 版本 - Java 25,将于今年 9 月份发布。四年前记录过一篇 Java 10 ~ 16 一路向前冲(新特性一箩筐),其中夹杂着孵化,预览中的以及正式的特性。现在继续跟随着 Java 16 之后版本的特性,主要讲述正式的,可直观体验到的特性,孵化与预览中的特性只会简单提及。

先还是看下 Java 的发布日期安排 Oracle Java SE Support Roadmap

版本                     发布日                        原定支持至               延期支持至
Java 17(LTS)      2021/9                       2026/9                      2029/12
Java 18-20         2022/3 - 2023/3      2022/9 - 2023/9     N/A
Java 21(LTS)      2023/9                       2028/9                     2031/9
Java 22               2024/3                       2024/9                      N/A
Java 23               2024/9                       2025/3                      N/A
Java 24               2025/3                       2025/9                      N/A
Java 25(LTS)      2025/9                       2030/9                     2033/9

也就是每两年(9月份)会有一个 LTS 版本,中间若干过度版本。在正式项目中尽可能只用 LTS 版本,因为 LTS 版更稳定,且有长期的补丁,不能项目进行中不得不在进行非 LTS 主版本升级。

为了迎接两个月后的 Java 25(LTS) 版本,开始回顾总结在 Java 17 与 Java 24 之间的关键新特性。又要用到 IntelliJ IDEA 中显示的不同版本 Java 的 Language level 的一句话描述

 

转换成文字描述

  • 15 - Text blocks
  • 16 - Records, patterns, local enums and interfaces
  • 17 - Sealed types, always-strict floating-point semantics
  • 18 - JavaDoc snippets
  • 19, 20 - No new language features
  • 21 - Record patterns, pattern matching for switch, (Preview) - String templates, unnamed classes and instance main methods, etc.
  • 22 - Unnamed variables and patterns
  • 23 - Markdown documentation comments, (Preview) - Primitive types in patterns, implicitly declared classes, etc.
  • 24 - Stream gatherers, (Preview) - Flexible constructor bodies, simple sources files, etc. 

探索 Java 17 的新特性依然由官方的 What's New in JDK 17 - New Features and Enhancements 开始

JEP 409: 密封类型(Sealed classes)

在 Kotlin, Scala 和 C# 都有 sealed 类型。Java 17 的 Sealed 类型有以下几个特性

  1. sealed 可用于修饰的类或接口
  2. sealed 声明的类或接口,必须有子类型
  3. sealed 类型的子类型必须使用 non-sealed, final, 或 sealed 修饰(non-sealed 子类可衍生孙子类, final 则不可再派生, sealed 则进一步要求明确的子类)
  4. sealed 类型用  permits 指定的子类型的当然是已定义的

密封类型的用意大约是要创建一个基类以及有限的已知子类。在没有密封类型的情况下,只能用 final 去修饰子类型,例如

由于作为 SDK 使用, 所以 Shape, Circle, 和  Square  都需要声明为 public, 虽然 Circle 和  Square 是 final, 但仍然无法阻止由 Shape 衍生出其他的子类。有了 sealed 关键字的话就可以改为

这样就能彻底杜绝再创建新的 Shpe, Circle, 或  Square 的子类了。

密封类涉及到 sealed, non-sealed, permits, 以及已有的 final 关键字。

Java 在防止创建子类的方式还有一些,如 final 类型完全杜绝子类,package-private 构造函数只允许同包中创建子类(但会影响外部访问基类信息),private 构造函数比 final 类还严格些。

Sealed 类型有些像枚举,只不过后者限定的是有限的实例,sealed 要达成的目的是有限的子类。

逐步理解密封类型

我们可通过在 IntelliJ IDEA 中的提示错误来感受 Sealed Classes 的用法

首先我们只引用 sealed 关键字,只写出下面的代码

IntelliJ IDEA 会有 Shape 的错误提示

Sealed class must have subclasses
Make 'Shape' non-sealed

这里所说的 non-sealed 其实是说移除 sealed 关键字,而非真把 sealed 换成 non-sealed,如此写成

又会看到对 non-sealed 的错误提示

Modifier 'non-sealed' is not allowed on classes that do not have a sealed superclass

所以必须要给它声明一个 subclass

这时候对 Rectangle 有错误提示

Modifier 'sealed', 'non-sealed' or 'final' expected

相应的有以下三种选择

final 表示 Rectangle 不可被继承

non-sealedRectangle 成为一个普通,可被任意继承的类,重新开放,如由它创建一个 Square 的子类

sealed 又让 Rectangle 又像之前的 Shape 那样要求有子类的编译错误

Sealed class must have subclasses 

这时候还没有引入 permits 关键字,其实下面两种写法是等效的

permits 关键字在基类 Shape 这一层就明确了允许的子类,而不需要到子类中寻找。建议不要省略 permits 关键字。

如果少量的实现代码可写在一个源文件中,如

如果希望 Shape 为抽象类,就加上 abstract 关键字

Sealed Class 与包,模块

sealed 类与 permits 的子类必须在同一模块(named module) 或相同的包( package - unnamed module)。比如我们在不同的包中分别创建 Shape 和 Circle 类

我们看到上面两段代码的错误都是

Class 'b.Circle' from another package not allowed to extend sealed class 'a.Shape' in unnamed module

要么放在同一个包中(unnamed module), 如果我们创建一个 named module, Shape 和  Circle 就能在不同的 package 了。

以 Maven 项目为例,具体做法是创建下面的文件布局

module-info.java 中内容为

命令模块名为 xyz

Shape.java 代码

Circle.java 代码

这样编译就没问题了,想要使用 Circle 类,可创建 src/main/java/com/example/Client.java

正常使用模块 xyz 中的 Circle 类。

sealed 关键也可用于 interface,只需要注意 interface 与 class 的区别就是了,例如下面代码演示了 sealed interface 的用法

由于 record 天生是 final, 所以实现 Shape 的 Triangle 不用再写 final。

Sealed Class 与模式匹配

这要穿越到 Java 21,因为 Java 21 才开始正式支持模式匹配。Sealed Class 像枚举一样有着限定数目的类型,所以针对前面的代码,我们可以写下面 switch-case

正如模式对于枚举类型在编译期就能推断出有没有覆盖所有的选项,没有则必需要有 default 分支,对于 Sealed Class 也一样,编译期也知道所有的子类型,所以这段代码覆盖了所有的分支,无须 default, 可通过编译。

Sealed Class 与字节码,反射

Sealed Class 编译产生的字节码中有一个 PermittedSubclasses 表示了它是不是一个 Sealed Class, 有值(非空数组)则是, null 值则不是。

测试下面的代码,String 为非 Sealed Class, Shape 为 Sealed Class

执行的输出为

null
false
[Ljava.lang.Class;@2e0fa5d3
true

JDK 中的 Sealed class

在 java.lang.constant 包中

它封装了 JVM class 文件中所有合法的常量描述类型,对于我们编写代码好像没什么用。

小结 Sealed class 的几条约束

基本是从 JEP 409 中转述的内容

    1. sealed 类与 permits 允许的子类必须在同一个模块中(named module), 或在同一个包中(unnamed module)
    2. 每一个 permits 子类必须直接继承自 sealed 类
    3. 每个 permits 子类必须使用 final, sealednon-sealed 中的一个关键字修饰
      • final 修饰的让该子类不再被继承(record 暗含着 final)
      • sealed 让子类又成为一个 sealed class, 将适用一样的 sealed 类规则
      • non-sealed 又让子类复原为普通的类,即解除了 sealed 的限制。就像没有用 sealed 修饰的类一样(class Person),它将不在对任何未知子类对它的继承。 

Sealed Class 对我们以后使用框架编写应用似乎没多大用处,理解它将有助于我们读懂第三方库使用了 sealed 特性的代码,或者自己要写 SDK 时又不希望使用者任意发挥时有些帮助。

通过写本文顺便把自 Java 9 开始引入的模块概念也略微有些体验。本想一文综述 Java 17 的特别关键新特性,没想第一个 Sealed Class 就占满了篇幅,所以不得已到此为止。

本文链接 https://yanbin.blog/java-17-new-features-sealed-classes/, 来自 隔叶黄莺 Yanbin Blog

[版权声明] Creative Commons License 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。

Subscribe
Notify of
guest

1 Comment
Inline Feedbacks
View all comments
trackback

[…] Java 17 新特性之密封类型, 继续刷 What's New in JDK 17 - New Features and […]