工作中所有项目都已升级到了当前的 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 类型有以下几个特性
- sealed 可用于修饰的类或接口
- sealed 声明的类或接口,必须有子类型
- sealed 类型的子类型必须使用 non-sealed, final, 或 sealed 修饰(non-sealed 子类可衍生孙子类, final 则不可再派生, sealed 则进一步要求明确的子类)
- sealed 类型用 permits 指定的子类型的当然是已定义的
密封类型的用意大约是要创建一个基类以及有限的已知子类。在没有密封类型的情况下,只能用 final 去修饰子类型,例如
1 2 3 4 5 6 7 8 |
// Shape.java public abstract Shape {} // Circle.java public final class Circle extends Shape {} // Square.java public final class Square extends Shape {} |
由于作为 SDK 使用, 所以 Shape, Circle, 和 Square 都需要声明为 public, 虽然 Circle 和 Square 是 final, 但仍然无法阻止由 Shape 衍生出其他的子类。有了 sealed 关键字的话就可以改为
1 2 3 4 5 6 7 8 |
// Shape.java public sealed class Shape permits Circle, Square {} // Circle.java public final class Circle extends Shape {} // Square.java public final class Square extends Shape {} |
这样就能彻底杜绝再创建新的 Shpe, Circle, 或 Square 的子类了。
密封类涉及到 sealed
, non-sealed
, permits
, 以及已有的 final
关键字。
Java 在防止创建子类的方式还有一些,如 final 类型完全杜绝子类,package-private 构造函数只允许同包中创建子类(但会影响外部访问基类信息),private 构造函数比 final 类还严格些。
Sealed 类型有些像枚举,只不过后者限定的是有限的实例,sealed 要达成的目的是有限的子类。
逐步理解密封类型
我们可通过在 IntelliJ IDEA 中的提示错误来感受 Sealed Classes 的用法
首先我们只引用 sealed
关键字,只写出下面的代码
1 |
public sealed class Shape {} |
IntelliJ IDEA 会有 Shape
的错误提示
Sealed class must have subclasses
Make 'Shape' non-sealed
这里所说的 non-sealed
其实是说移除 sealed
关键字,而非真把 sealed
换成 non-sealed
,如此写成
1 |
public non-sealed class Shape {} |
又会看到对 non-sealed
的错误提示
Modifier 'non-sealed' is not allowed on classes that do not have a sealed superclass
所以必须要给它声明一个 subclass
1 2 3 4 |
public sealed class Shape { } class Rectangle extends Shape {} |
这时候对 Rectangle
有错误提示
Modifier 'sealed', 'non-sealed' or 'final' expected
相应的有以下三种选择
1 2 3 |
final class Rectangle extends Shape {} non-sealed class Rectangle extends Shape {} sealed class Rectangle extends Shape {} |
用 final
表示 Rectangle
不可被继承
non-sealed
让 Rectangle
成为一个普通,可被任意继承的类,重新开放,如由它创建一个 Square 的子类
sealed
又让 Rectangle
又像之前的 Shape 那样要求有子类的编译错误
Sealed class must have subclasses
这时候还没有引入 permits
关键字,其实下面两种写法是等效的
|
|
用 permits
关键字在基类 Shape 这一层就明确了允许的子类,而不需要到子类中寻找。建议不要省略 permits
关键字。
如果少量的实现代码可写在一个源文件中,如
1 2 3 4 |
public abstract sealed class Shape { final class Rectangle extends Shape {} final class Circle extends Shape {} } |
如果希望 Shape 为抽象类,就加上 abstract
关键字
Sealed Class 与包,模块
sealed 类与 permits 的子类必须在同一模块(named module) 或相同的包( package - unnamed module)。比如我们在不同的包中分别创建 Shape 和 Circle 类
1 2 3 |
package a; public abstract sealed class Shape permits b.Circle{} |
1 2 3 |
package b; public final class Circle extends a.Shape {} |
我们看到上面两段代码的错误都是
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 项目为例,具体做法是创建下面的文件布局
1 2 3 4 5 6 7 8 9 10 11 |
. ├── pom.xml └── src └── main └── java ├── module-info.java └── xyz ├── a │ └── Shape.java └── b └── Circle.java |
module-info.java 中内容为
1 2 |
module xyz { } |
命令模块名为 xyz
Shape.java 代码
1 2 3 4 5 6 |
package xyz.a; import xyz.b.Circle; public abstract sealed class Shape permits Circle { } |
Circle.java 代码
1 2 3 4 5 6 |
package xyz.b; import xyz.a.Shape; public final class Circle extends Shape { } |
这样编译就没问题了,想要使用 Circle 类,可创建 src/main/java/com/example/Client.java
1 2 3 4 5 6 7 8 9 |
package com.example; import xyz.b.Circle; public class Client { public static void main(String[] args) { System.out.println(new Circle()); } } |
正常使用模块 xyz 中的 Circle 类。
sealed
关键也可用于 interface,只需要注意 interface 与 class 的区别就是了,例如下面代码演示了 sealed interface 的用法
1 2 3 4 5 |
sealed interface Shape permits Circle, Rectangle, Triangle {} final class Circle implements Shape {} non-sealed class Rectangle implements Shape {} final class Square extends Rectangle {} record Triangle(float x, float y, float z) implements Shape {} |
由于 record 天生是 final, 所以实现 Shape 的 Triangle 不用再写 final。
Sealed Class 与模式匹配
这要穿越到 Java 21,因为 Java 21 才开始正式支持模式匹配。Sealed Class 像枚举一样有着限定数目的类型,所以针对前面的代码,我们可以写下面 switch-case
1 2 3 4 5 6 7 |
String matchName(Shape shape) { return switch (shape) { case Circle c -> "Circle"; case Square s -> "Square"; case Rectangle r -> "Rectangle"; }; } |
正如模式对于枚举类型在编译期就能推断出有没有覆盖所有的选项,没有则必需要有 default
分支,对于 Sealed Class 也一样,编译期也知道所有的子类型,所以这段代码覆盖了所有的分支,无须 default
, 可通过编译。
Sealed Class 与字节码,反射
Sealed Class 编译产生的字节码中有一个 PermittedSubclasses 表示了它是不是一个 Sealed Class, 有值(非空数组)则是, null 值则不是。
测试下面的代码,String 为非 Sealed Class, Shape 为 Sealed Class
1 2 3 4 |
System.out.println(String.class.getPermittedSubclasses()); System.out.println(String.class.isSealed()); System.out.println(Shape.class.getPermittedSubclasses()); System.out.println(Shape.class.isSealed()); |
执行的输出为
null
false
[Ljava.lang.Class;@2e0fa5d3
true
JDK 中的 Sealed class
在 java.lang.constant 包中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
package java.lang.constant; public sealed interface ConstantDesc permits ClassDesc, MethodHandleDesc, MethodTypeDesc, Double, DynamicConstantDesc, Float, Integer, Long, String { ...... } |
它封装了 JVM class 文件中所有合法的常量描述类型,对于我们编写代码好像没什么用。
小结 Sealed class 的几条约束
基本是从 JEP 409 中转述的内容
-
- sealed 类与 permits 允许的子类必须在同一个模块中(named module), 或在同一个包中(unnamed module)
- 每一个 permits 子类必须直接继承自 sealed 类
- 每个 permits 子类必须使用
final
,sealed
和non-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
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。
[…] Java 17 新特性之密封类型, 继续刷 What's New in JDK 17 - New Features and […]