16.Kotlin 类:类的形态(三):密封类 (Sealed Class)

希望帮你在Kotlin进阶路上少走弯路,在技术上稳步提升。当然,由于个人知识储备有限,笔记中难免存在疏漏或表述不当的地方,也非常欢迎大家提出宝贵意见,一起交流进步。 ------ Android_小雨

整体目录:Kotlin 进阶不迷路:41 个核心知识点,构建完整知识体系

一、前言

Kotlin 官方对密封类的完整定义(逐句拆解)

Sealed classes and interfaces represent restricted class hierarchies that provide more control over inheritance. All direct subclasses of a sealed class are known at compile time. No other subclasses may appear outside the module and the file where the sealed class is defined. Sealed classes are, in fact, enumerated classes with super-powers.

官方强调的 5 个核心点(记住这 5 条就掌握了密封类本质):

  1. 密封类 = 受限的类层次结构(restricted hierarchy)
  2. 所有直接子类在编译期就完全已知
  3. 子类必须定义在同一个文件 中(Kotlin 1.0-1.4)→ Kotlin 1.5+ 放宽到同一个模块 + 同一个编译单元
  4. 不能在密封类外部再添加子类
  5. 密封类是"增强版的枚举类"(enum with superpowers)

一句话总结:密封类就是编译器知道所有可能子类型的类,专为"有限种状态"而生。

1.1 密封类的核心定位(限制继承范围、规范有限类型集合)

在面向对象编程中,我们经常面临一个矛盾:既想要多态的灵活性,又想要枚举的确定性。 Kotlin 官方对密封类的定义非常精准:"Sealed classes represent restricted class hierarchies"。 翻译成开发者听得懂的话就是:密封类是专为"有限种状态"而生的受限类层级。它核心只做一件事------把继承的权利关进笼子里,不让外部随意扩展。

1.2 Kotlin 密封类的设计价值(兼顾灵活性与安全性,编译时校验类型)

密封类被称为"增强版的枚举"(Enum with superpowers)。

  • 相比枚举 :它更灵活。枚举的每个实例结构都必须一样,而密封类的子类可以是单例(object)、数据类(data class)甚至是普通类,每种状态都能携带自己独有的数据。
  • 相比接口/抽象类 :它更安全。编译器在编译期就知道它所有的子类,这意味着当你用 when 处理它时,编译器能帮你做"穷尽检查",杜绝逻辑遗漏。

1.3 核心疑问:Kotlin 密封类编译后是什么样?

很多兄弟好奇:Java 直到 15/17 才引入 sealed,那 Kotlin 早几年是怎么在 JVM 上跑起来的?它是黑科技吗? 完全不是。 只要我们扒开字节码(Decompile)看一眼,就会发现它本质上利用了 Java 的访问控制机制。

1.4 本文核心内容预告

本文将从底层原理出发,层层递进:

  1. Java 映射:看透它的底层实现。
  2. 语法规范 :掌握 2025 年最新的定义方式(如 data object)。
  3. 核心特性:理解为什么它能做编译期检查。
  4. 实战场景:UI 状态、网络请求、导航路由等真实案例。
  5. 对比分析:彻底搞懂它和 Enum 的区别。

二、核心揭秘:Kotlin 密封类 → Java 代码映射

要真正理解密封类,最直接的方法是看它编译后的字节码对应什么 Java 代码。

2.1 映射底层原理

Kotlin 的 sealed class 在编译成 Java 字节码时,主要做了两件事:

  1. 转变为抽象类 :密封类本身会被编译为一个 abstract class,因此不能直接实例化。
  2. 私有化/受限构造函数 :它的构造函数通常是 privateprotected,并在元数据中标记只允许在特定范围内继承。

2.2 完整示例映射

2.2.1 Kotlin 原代码

kotlin 复制代码
sealed class Result {
    data class Success(val msg: String) : Result()
    data object Error : Result() // Kotlin 1.9+ 推荐写法
}

2.2.2 对应的 Java 反编译结果(核心逻辑)

java 复制代码
// 1. 本质就是个抽象类
public abstract class Result {

    // 2. 关键点:构造函数是私有的!
    private Result() {}

    // 3. Kotlin 生成的合成构造器,用于允许内部子类调用
    // 逻辑上就是锁死了外部继承的可能性
    public /* synthetic */ Result(DefaultConstructorMarker marker) {
        this();
    }

    // 子类 1:静态内部类
    public static final class Success extends Result { ... }

    // 子类 2:静态单例
    public static final class Error extends Result { ... }
}

2.3 关键细节说明

结论就一句话:密封类本质上就是一个"构造函数被私有化"的抽象类。 因为构造函数被锁死了,编译器可以保证除了它允许的"亲儿子"(同文件或同模块内的类),其他任何地方的代码都无法继承它。这就是"有限继承"的底层实现。

三、密封类基础:定义与语法规范

3.1 基本语法

使用 sealed 关键字修饰类即可。

kotlin 复制代码
sealed class UiState

3.2 子类定义规则

随着 Kotlin 版本的迭代,规则逐渐放宽:

  • Kotlin 1.0 - 1.4:子类必须定义在密封类内部,或者同一文件中。
  • Kotlin 1.5+(当前主流)只要在同一个模块(Module)且同一个包(Package)内 ,子类可以写在不同的文件里。
    • 实战建议:虽然允许分文件写,但为了代码可读性,建议除非类特别大,否则还是写在同一个文件里,一目了然。

3.3 简单示例(包含 2025 年最新语法)

注意:对于没有数据的状态,Kotlin 1.9+ 强烈推荐使用 data object 替代普通的 object,以便获得更好的 toString 输出。

kotlin 复制代码
// 文件:UiState.kt
sealed class UiState<out T> { // 支持泛型
    // 1. 纯状态:推荐用 data object
    data object Loading : UiState<Nothing>()

    // 2. 带数据:用 data class
    data class Success<T>(val data: T) : UiState<T>()

    // 3. 带异常:用 data class
    data class Error(val exception: Throwable) : UiState<Nothing>()
}

3.4 关键要求

  1. 禁止外部新增子类:你不能在另一个模块或非同包下定义子类。
  2. 子类不可为密封类自身:这会导致循环继承逻辑错误。
  3. 自身抽象 :不能直接 new UiState()

四、密封类的核心特性

4.1 有限继承

密封类定义了一个封闭的类型集合。这不仅仅是代码组织的方式,更是对业务逻辑的强约束。比如"网络请求"只有成功、失败、加载中这三种状态,绝不可能出现第四种未知的状态。

4.2 不可实例化

由于编译后是抽象类,你无法创建密封类本身的实例。你只能创建其子类的实例。

4.3 when 表达式穷尽匹配(官方最强杀器)

这是密封类存在的最大理由!当你在 when 表达式中使用密封类时:

kotlin 复制代码
fun handle(state: UiState<String>) {
    when (state) {
        is UiState.Loading -> showLoading()
        is UiState.Success -> showText(state.data)
        is UiState.Error -> showToast(state.exception.message)
    }
    // 注意:这里不需要写 else!
}

编译期安全 :如果你漏写了 Error 分支,编译器会直接报错 'when' expression must be exhaustive。这比运行时崩溃强一万倍。

4.4 支持带构造函数

密封类可以拥有构造函数(默认 protectedprivate),允许你在父类中定义通用的属性,由子类传递进来。

4.5 子类可多实例

  • data object 子类是单例。
  • data class / class 子类可以创建无数个实例,每个实例携带不同的数据(如不同的错误信息、不同的用户数据)。

五、密封类的使用场景与实战

5.1 状态管理(UI State)

这是 Android Jetpack Compose 或 ViewModel 中最标准的写法。

kotlin 复制代码
sealed class ScreenState {
    data object Loading : ScreenState() // 页面加载中
    data class Content(val userList: List<User>) : ScreenState() // 显示内容
    data class Error(val message: String) : ScreenState() // 显示错误页
}

5.2 有限类型场景(支付/权限)

比如处理权限申请结果,这是典型的有限集合:

kotlin 复制代码
sealed interface PermissionResult {
    data object Granted : PermissionResult
    data object Denied : PermissionResult
    data class Rationale(val shouldShow: Boolean) : PermissionResult
}

5.3 when 表达式完美适配

结合 when 表达式,业务逻辑变得异常清晰,且不用担心遗漏。

5.4 完整实战示例:页面导航(Navigation)

在 Compose 或传统路由中,用密封类管理页面参数是最优雅的。

kotlin 复制代码
sealed class Screen(val route: String) {
    data object Home : Screen("home")
    data object Login : Screen("login")
    // 携带参数的页面
    data class Detail(val id: Long) : Screen("detail/$id")
}

fun navigate(screen: Screen) {
    when(screen) {
        Screen.Home -> navigator.push("home")
        is Screen.Detail -> navigator.push("detail/${screen.id}")
        // 编译器强制检查所有页面
        else -> {}
    }
}

六、密封类与枚举 (Enum) 的核心区别

6.1 实例特性

  • 枚举 (Enum)单例的Color.RED 全局只有一个。
  • 密封类子类实例独立的Success(data1)Success(data2) 是两个不同的对象。

6.2 扩展灵活性

  • 枚举:所有枚举常量的结构必须完全一致。
  • 密封类:子类可以各玩各的。一个是单例,一个是复杂的数据类,互不干扰。

6.3 适用场景对比表

特性 枚举 (Enum Class) 密封类 (Sealed Class)
状态数量 固定有限 类型固定,但实例无限
数据结构 所有实例结构必须相同 每个子类可以有完全不同的属性
内存开销 最小 稍大(因为要创建对象)
典型场景 星期、颜色、方向 UI 状态、网络结果、指令集

七、密封类的进阶用法

7.1 子类携带参数与泛型

配合协变泛型 out TNothing 类型,可以写出极其通用的结果类。

kotlin 复制代码
sealed class Result<out T> {
    data class Success<T>(val data: T) : Result<T>()
    // Error 不带泛型数据,继承 Result<Nothing> 即可兼容所有泛型
    data class Error(val e: Throwable) : Result<Nothing>()
}

7.2 密封类与数据类结合

99% 的情况下,密封类的子类都应该是 data classdata object。 这样你会自动获得 equals()hashCode()toString()copy(),在调试和对比状态时非常有用。

7.3 嵌套子类

处理层级化的错误码时很有用:

kotlin 复制代码
sealed class AppError : Exception() {
    data object Unknown : AppError()

    sealed class Network : AppError() {
        data object Timeout : Network()
        data class HostUnreachable(val url: String) : Network()
    }

    sealed class Database : AppError() {
        data object DiskFull : Database()
    }
}

八、使用注意事项与避坑点

8.1 子类必须在同一文件(或同一模块)

虽然 Kotlin 1.5+ 允许子类定义在同模块的不同文件中,但强烈建议对于简单的状态类,仍然写在同一个文件中。这能让维护者一眼看全所有可能的状态,保持代码的"内聚性"。

8.2 when 匹配需穷尽所有子类

避坑指南 :在 when 匹配密封类时,尽量不要写 else 分支 。 如果你写了 else,当你日后新增了一个子类状态(比如新增了 Loading),编译器就不会报错提醒你。这会导致新状态在运行时被忽略,产生隐蔽的 Bug。

8.3 避免滥用

密封类适合**类型可枚举(有限)**的场景。如果你的子类数量成百上千,或者需要支持第三方插件动态扩展子类,请使用普通的 interfaceabstract class

九、总结与最佳实践

9.1 核心知识点回顾

  • 定义sealed class = 编译期已知的受限类层级。
  • 本质:私有构造函数的抽象类。
  • 杀手锏when 表达式的穷尽性检查。

9.2 最佳实践

  1. UI 状态必用:在 ViewModel 中定义 State 时,Sealed Class 是标准答案。
  2. 单例用 data object :Kotlin 1.9+ 后,无参数状态请使用 data object 而非 object
  3. 拒绝 else:让编译器做你的僚机,通过报错来提醒你处理新状态。

9.3 选型建议

  • 如果是一组固定的常量(如星期一到星期日)→ Enum
  • 如果是一组固定的类型,且每种类型需要携带不同的数据(如成功带数据,失败带异常)→ Sealed Class
  • 如果类型需要无限扩展 → Interface / Abstract Class

掌握了密封类,你就掌握了 Kotlin 类型安全的精髓,从此告别运行时的一脸懵逼,拥抱编译时的稳如泰山。

相关推荐
马卡巴卡43 分钟前
MySQL权限管理的坑你踩了没有?
后端
4***175444 分钟前
Spring Boot整合WebSocket
spring boot·后端·websocket
Penge66644 分钟前
Elasticsearch 集群必看:为什么 3 个 Master 节点是生产环境的 “黄金配置”?
后端
Java水解44 分钟前
MyBatis 源码深度解析:从 Spring Boot 实战到底层原理
后端·mybatis
随风飘的云1 小时前
es搜索引擎的持久化机制原理
后端
Se7en25812 小时前
基于 MateChat 构建 AI 编程智能助手的落地实践
后端
n***F8752 小时前
Skywalking介绍,Skywalking 9.4 安装,SpringBoot集成Skywalking
spring boot·后端·skywalking
w***37512 小时前
SpringBoot【实用篇】- 测试
java·spring boot·后端
年小个大2 小时前
优化App启动时间?startup-coroutine是什么?
性能优化·架构·kotlin