Kotlin 密封类与延迟初始化学习笔记

Kotlin 密封类与延迟初始化学习笔记


目录

  1. [密封类 Sealed Class](#密封类 Sealed Class)
  2. 延迟初始化
  3. [lateinit vs lazy](#lateinit vs lazy)

1. 密封类 Sealed Class

什么是密封类?

密封类是一种受限的类继承结构,所有子类必须在同一个文件中声明。

为什么需要密封类?

场景 问题 解决
when 表达式 用 enum 时子类数量固定 枚举
when 表达式 用普通类时无法穷尽检查 密封类

sealed vs enum

对比 enum sealed
子类数量 固定 可任意数量
子类状态 每个枚举值是单例 可带构造参数
扩展性 不能继承 enum 不能继承 sealed
使用场景 有限集合 类型安全分支

基本语法

kotlin 复制代码
// sealed 修饰符
sealed class Result {
    class Success(val data: String) : Result()
    class Error(val message: String) : Result()
    object Loading : Result()  // object 表示单例
}

使用示例

kotlin 复制代码
// 定义密封类
sealed class Result<out T> {
    data class Success<T>(val data: T) : Result<T>()
    data class Error(val message: String) : Result<Nothing>()
    object Loading : Result<Nothing>()
}

// when 表达式(编译器会检查是否穷尽)
fun handleResult(result: Result<String>) {
    when (result) {
        is Result.Success -> println("成功: ${result.data}")
        is Result.Error -> println("失败: ${result.message}")
        is Result.Loading -> println("加载中...")
        // 不需要 else,因为所有情况都已覆盖
    }
}

密封类的特点

kotlin 复制代码
sealed class Expr {
    class Num(val value: Int) : Expr()
    class Sum(val left: Expr, val right: Expr) : Expr()
}

// 1. 不能被外部实例化
// val e = Expr()  // 错误

// 2. 子类必须在这个文件里定义
class MyExpr : Expr()  // 错误:不能继承 sealed 类

// 3. 子类可以有不同的形态
val num = Expr.Num(10)           // 带参数
val sum = Expr.Sum(num, Expr.Num(20))  // 嵌套

应用场景

场景一:UI 状态
kotlin 复制代码
sealed class UiState<out T> {
    object Loading : UiState<Nothing>()
    data class Success<T>(val data: T) : UiState<T>()
    data class Error(val msg: String) : UiState<Nothing>()
}

// 使用
val state: UiState<List<String>> = UiState.Success(listOf("A", "B"))

when (state) {
    is UiState.Loading -> showLoading()
    is UiState.Success -> showData(state.data)  // data 类型已知
    is UiState.Error -> showError(state.msg)
}
场景二:命令模式
kotlin 复制代码
sealed class Command {
    class Add(val item: String) : Command()
    class Remove(val index: Int) : Command()
    object Clear : Command()
}

fun execute(command: Command) {
    when (command) {
        is Command.Add -> addItem(command.item)
        is Command.Remove -> removeItem(command.index)
        is Command.Clear -> clearAll()
    }
}
场景三:导航结果
kotlin 复制代码
sealed class NavigationResult {
    object Success : NavigationResult()
    object Cancelled : NavigationResult()
    data class Error(val code: Int) : NavigationResult()
}

2. 延迟初始化

为什么需要延迟初始化?

kotlin 复制代码
class MyActivity : AppCompatActivity() {
    private var adapter: MyAdapter  // 不能直接赋值

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // adapter 必须在 setContentView() 之后才能初始化
        adapter = MyAdapter()  // 太晚了?或者太早了?
    }
}

lateinit

kotlin 复制代码
class MyActivity : AppCompatActivity() {

    private lateinit var adapter: MyAdapter  // 延迟初始化声明

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 在合适的时候初始化
        adapter = MyAdapter()
    }

    fun getAdapter() = adapter  // 使用时保证已初始化
}

lateinit 规则

kotlin 复制代码
// 1. 只能修饰 var
lateinit var name: String    // ✓
lateinit val age: Int        // ✗ val 不可变

// 2. 不能用于基本类型(Int、Boolean 等)
lateinit var count: Int      // ✗ 基本类型用 var + 延迟
lateinit var name: String    // ✓ 对象类型

// 3. 必须手动初始化
lateinit var adapter: MyAdapter
// adapter.doSomething()  // 错误:还没初始化

// 4. 判断是否已初始化
if (::adapter.isInitialized) {
    adapter.doSomething()
}

lateinit 使用场景

kotlin 复制代码
class MainActivity : AppCompatActivity() {

    private lateinit var recyclerView: RecyclerView
    private lateinit var adapter: FruitAdapter
    private lateinit var toolbar: MaterialToolbar

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 初始化 ViewBinding
        binding = ActivityMainBinding.inflate(layoutInflater)

        // 初始化控件
        recyclerView = findViewById(R.id.recyclerView)
        adapter = FruitAdapter(dataList)
        toolbar = findViewById(R.id.toolbar)
    }
}

3. lateinit vs lazy

对比表

特性 lateinit lazy
修饰符 var val
首次访问 手动初始化 自动初始化
线程安全 是(默认)
基本类型 不可用 可用(by lazy { 0 }
内存 保留声明时的内存 只在首次访问时分配

lazy 延迟属性

kotlin 复制代码
class User {
    // val 不可变,用 lazy 延迟初始化
    val database: Database by lazy {
        println("初始化数据库...")
        Database()
    }

    val config: Map<String, String> by lazy {
        loadConfig()
    }
}

// 第一次访问时才初始化
val user = User()
println(user.database)  // 输出: 初始化数据库...,然后返回 Database 实例
println(user.database)  // 直接返回已有的实例,不再打印

lazy 线程安全

kotlin 复制代码
// 默认线程安全
val db1 by lazy { Database() }

// 非线程安全
val db2 by lazy(LazyThreadSafetyMode.NONE) { Database() }

// PUBLICATION:多个线程可能初始化多次(最终取其中一个)
val db3 by lazy(LazyThreadSafetyMode.PUBLICATION) { Database() }

场景对比

kotlin 复制代码
// 场景一:Activity/Fragment 中的 View(用 lateinit)
class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)  // 必须手动
    }
}

// 场景二:单例或配置(用 lazy)
class App : Application() {
    val database by lazy { Database.getInstance() }
    val config by lazy { Config.load() }
}

// 场景三:RecyclerView Adapter(用 lateinit)
class MainActivity : AppCompatActivity() {
    private lateinit var adapter: FruitAdapter

    override fun onCreate(...) {
        adapter = FruitAdapter(dataList)  // 手动
    }
}

// 场景四:复杂对象的延迟加载(用 lazy)
class UserProfile {
    // 头像图片较大,延迟加载
    val avatarBitmap by lazy {
        BitmapFactory.decodeFile(avatarPath)
    }

    // 用户相册可能很大,按需加载
    val photoGallery by lazy {
        loadPhotosFromDisk()
    }
}

常见错误

kotlin 复制代码
// 错误 1:val 用 lateinit
lateinit val name: String  // ✗

// 错误 2:基本类型用 lateinit
lateinit var count: Int    // ✗

// 错误 3:未初始化就使用
lateinit var adapter: MyAdapter
adapter.notifyDataSetChanged()  // ✗ 可能崩溃

// 正确:初始化后才能使用
lateinit var adapter: MyAdapter
adapter = MyAdapter()  // 先初始化
adapter.notifyDataSetChanged()  // ✓

最佳实践

kotlin 复制代码
class MyActivity : AppCompatActivity() {

    // ViewBinding - 必须用 lateinit(var)
    private lateinit var binding: ActivityMainBinding

    // 只读配置 - 用 lazy
    private val apiUrl by lazy { BuildConfig.API_URL }
    private val maxCacheSize by lazy { 100 * 1024 * 1024 }

    // 数据适配器 - 用 lateinit(可能需要重新创建)
    private lateinit var adapter: FruitAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        adapter = FruitAdapter(emptyList())
        recyclerView.adapter = adapter
    }
}

附录:选型指南

什么时候用 sealed?

场景 推荐
有限状态(加载/成功/失败) sealed
导航结果 sealed
命令模式 sealed
配置选项 enum(更简单)

什么时候用 lateinit?

场景 推荐
ViewBinding lateinit var
Adapter lateinit var
需要重新赋值的对象 lateinit var

什么时候用 lazy?

场景 推荐
单例实例 by lazy
配置常量 by lazy
重量级对象(数据库、Bitmap) by lazy
只读属性 by lazy

相关推荐
Aliex_git2 小时前
前端监控笔记(三)
前端·笔记·学习
朝星2 小时前
Android开发[3]:协程+Flow
android·kotlin
im_AMBER2 小时前
Leetcode 159 无重复字符的最长子串 | 长度最小的子数组
javascript·数据结构·学习·算法·leetcode
三品吉他手会点灯2 小时前
C语言学习笔记 - 2.C概述 - HelloWorld程序举例
c语言·笔记·学习
Rousson2 小时前
硬件学习笔记-97 不同存储器件简单介绍
笔记·学习
sheeta19982 小时前
LeetCode 每日一题笔记 日期:2026.04.20 题目:2078.两栋颜色不同而距离最远的房子
笔记·算法·leetcode
Fate_I_C2 小时前
Kotlin数据类equals和 == 会返回true
kotlin
Fate_I_C2 小时前
实战案例:用 Kotlin 重写一个 Java Android 工具类
android·java·kotlin
Jet7692 小时前
2026年API中转平台选型笔记:稳定性、兼容性、成本怎么一起看
java·网络·笔记