登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  java教程

Java sealed class 与反射检查的类型边界

来源:17golang原创

时间:2026-10-28 11:36:59 164浏览 收藏

Java sealed class 可以通过 Class.isSealed() 判断是否封闭,再用 getPermittedSubclasses() 读取直接许可子类型。但这个数组不是所有后代类的清单。遇到 non-sealed 分支后,后续继承仍然开放。

判断对象是否属于父类型,使用 isInstance;理解父类型允许谁直接继承,读取许可列表。直接许可、任意后代和精确类型是三个不同的问题。

我在设计这种检查时,会先把需要的类型关系写清,再选 API。下面用一个原创模型解释这一取舍,示例没有被描述成本机运行结果。

Java 官方文档地址:https://docs.oracle.com/en/java/javase/25/docs/api/

先把直接许可和整个继承树分开

假设任务结果 Result 允许 Done、Deferred、Extension 三个直接子类。Done 结束继承;Deferred 继续管理自己许可的下一层;Extension 则开放扩展。这种结构适合由库维护者控制主干,同时保留一个明确的扩展口。

不能一边允许 non-sealed 扩展,一边依赖根节点的许可数组穷举全部叶子类型。根节点控制的是自己的直接子类型,各分支如何继续扩展,要看分支自己的声明。

Result 直接许可与间接后代的结构关系
静态结构图:根类型的直接许可只覆盖第一层,后代关系由各分支自身的声明决定。

用三个分支建立最小模型

把下面内容保存为 SealedExample.java,相关类型都放在同一个包中。外层只有一个公开演示类,内部采用静态成员类,避免复制示例时出现多个 public 顶层类的文件名问题。

public class SealedExample {
    // 主干只允许这三个类型直接继承。
    public static sealed abstract class Result
            permits Done, Deferred, Extension { }
    public static final class Done extends Result { }

    // 封闭分支继续声明自己的直接许可子类。
    public static sealed abstract class Deferred extends Result
            permits Waiting { }
    public static final class Waiting extends Deferred { }

    // 扩展口的后代不由根节点的 permits 完整枚举。
    public static non-sealed class Extension extends Result { }
    public static final class VendorExtension extends Extension { }
}

Result 的直接许可对象是 Done、Deferred、Extension;Waiting 与 VendorExtension 是间接后代。Done 是 final,不能继续扩展;Deferred 仍然 sealed;Extension 是 non-sealed,允许其他后代继续加入,VendorExtension 只是这里给出的例子。

对类而言,许可子类必须直接继承,并通过 final、sealed 或 non-sealed 表达后续扩展方式。命名模块中,许可子类与父类应在同一模块;未命名模块中则要求同包。接口也支持 sealed,具体语法应按接口声明规则处理。这些属于声明约束,不能在反射时加一个参数来放宽。

读取许可列表时保留状态和加载限制

这两个 Class API 自 Java 17 提供。对非 sealed 类型,getPermittedSubclasses 返回 null;对 sealed 类型结果非 null,但允许为空数组。数组顺序没有保证;如果某个许可子类型无法取得 Class 对象,该项会被忽略。因此,返回数量不能证明部署中的全部类型文件完整可用。

下面的辅助类放到独立的 PermitView.java 文件。示例保留 sealed 状态,让“非封闭类型”和“当前取得零个许可类型的封闭类型”在业务表示中保持区别。

import java.util.Arrays;
import java.util.List;
import java.util.Objects;

public final class PermitView {
    public record View(boolean sealed, List directNames) { }

    public static View describe(Class> type) {
        Objects.requireNonNull(type, "类型不能为 null");
        if (!type.isSealed()) {
            // 列表为空不表示该类型禁止继承。
            return new View(false, List.of());
        }
        List names = Arrays.stream(type.getPermittedSubclasses())
                .map(Class::getName)
                .sorted() // 排序只服务稳定展示,不代表业务优先级。
                .toList();
        return new View(true, names);
    }
}

这里保存类名用于展示。如果后续要比较类型身份,应保留 Class 对象:不同类加载器可以加载相同二进制名称的类型,仅比较名字不足以表达运行时类型身份。也不要把数组第一项当作默认实现,默认分支应有自己的显式配置。

许可信息、对象归属和业务准入的检查关系
静态说明图:反射许可信息、对象归属和业务准入分别回答不同问题,不能互相替代。

检查实际对象时选择对应的关系

在最小模型中,Waiting 对象属于 Result,但 Waiting.class 不在 Result 的直接许可数组中。把对象的 getClass 与数组元素做相等比较,只能回答它是否恰好是某个直接许可类型,不能判断全部后代对象。

public final class ResultChecks {
    public static boolean belongsToResult(Object value) {
        // 接受间接后代;null 返回 false。
        return SealedExample.Result.class.isInstance(value);
    }

    public static boolean isExactlyDone(Object value) {
        // 精确类型规则独立命名,不与父类型归属混用。
        return value != null
                && value.getClass() == SealedExample.Done.class;
    }

    public static boolean allowsClass(Class> candidate) {
        // 判断方向是 Result 能否接收候选类型的实例。
        return candidate != null
                && SealedExample.Result.class.isAssignableFrom(candidate);
    }
}

第三个方法判断类型关系时,也会接受 Result.class 本身。它不承诺参数是具体、可实例化的实现类。如果任务是构造对象,还需要单独处理抽象类型、接口、构造器参数及可访问性,不能把 true 解释为可以直接调用无参构造器。

需要回答的问题采用的方式容易产生的误读
父类型是否封闭isSealed()false 不等于 final
直接允许谁继承getPermittedSubclasses()不是全部后代扫描
对象是否属于父类型isInstance(value)不代表业务授权
候选类型能否赋给父类型父类型.isAssignableFrom(候选类型)不保证可实例化
是否恰好是指定类getClass() == 指定类.class不会自动包含子类

把兼容性与业务准入放回各自的位置

本文声明与 API 以 Java 17 及以上为使用前提,引用 Java 25 API 文档说明返回行为。公共库如果还要运行在更早的 Java 上,应把低版本兼容实现和支持 sealed 的实现分开设计。仅在调用前加 if,不能解决新语法或较新字节码在旧运行时中的兼容问题。

允许继承 Result,只说明类型结构符合要求。对象是否来自可信扩展、是否具备某项操作权限、是否满足当前业务状态,需要业务层独立表达。尤其是 Extension 分支已经开放时,归属 Result 本身不能成为可信供应商的证明。

如果只是生成文档,可以展示可取得的直接许可关系,并沿 sealed 分支继续展开;遇到 non-sealed 就标注开放分支,不声称列举了全部实现。若目标是判断部署完整性,应依据构建产物清单和部署约定,不要让该反射 API 承担它没有承诺的职责。

isSealed 为 false,说明没有子类吗?

不能。开放类和 final 类都可能得到 false,但继承能力不同。如果任务需要区分它们,还应依据对应修饰符和声明语义判断。

递归读取许可列表可以得到所有后代吗?

只能沿当前可解析的 sealed 许可关系继续展开。遇到 non-sealed 分支后,无法凭 permits 找到它的所有后代。把结果叫作当前可解析的许可关系图更准确。

sealed 适合所有扩展接口吗?

我会把它用在希望明确控制的类型主干。面向任意第三方实现的接口,应保留相应扩展能力;像示例一样开放一个分支时,也要接受该分支无法被根节点完整枚举的事实。

相关官方资料:Class 许可子类 API、JLS 直接许可子类约束、Java 17 sealed 声明指南。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>