11、数据类与密封类:数据类的定义与使用、copy函数、解构声明、密封类的概念与使用
说实话,数据类和密封类这两个东西,是我在Kotlin项目里用得最频繁的特性之一。尤其是数据类,几乎每个实体类我都会考虑用它。今天我们就来好好聊聊它们。
数据类:不只是POJO那么简单
先说说数据类。在Java时代,我们写一个数据模型类,得手动生成getter、setter、toString、equals、hashCode……那叫一个繁琐。Kotlin的数据类,说白了就是帮我们把这一堆样板代码全干了。
定义一个数据类,只需要在class前面加个data关键字:
data class User(
val id: Long,
val name: String,
val email: String,
val age: Int = 0
)
就这么一行,编译器自动帮我们生成了:
equals()/hashCode()—— 基于主构造函数的所有属性toString()—— 格式是"User(id=1, name=张三, ...)"componentN()—— 按声明顺序生成,用于解构copy()—— 这个后面重点讲
copy函数:不可变对象的“修改”利器
我个人习惯把数据类的属性都声明为val,也就是不可变的。这样在多线程环境下特别安全。但问题来了——我想改某个字段怎么办?
这时候copy()函数就派上用场了。它不会修改原对象,而是创建一个新的对象,只替换你指定的字段:
val user1 = User(1, "张三", "zhangsan@example.com", 25)
val user2 = user1.copy(age = 26, name = "张三丰")
println(user1) // User(id=1, name=张三, email=zhangsan@example.com, age=25)
println(user2) // User(id=1, name=张三丰, email=zhangsan@example.com, age=26)
嗯,这里要注意:copy()是浅拷贝。如果属性里包含引用类型(比如List、自定义对象),拷贝的是引用,不是深层复制。我在项目中遇到过这个问题——一个数据类里有个MutableList,用copy之后两个对象共享同一个列表,改一个另一个也跟着变。后来我改成用不可变列表List才解决。
解构声明:把对象拆开用
解构声明是Kotlin一个很酷的特性。它允许你把一个对象拆成多个变量:
val (id, name, email, age) = user1
println("$id - $name - $email - $age")
为什么能这么写?因为数据类自动生成了component1()、component2()……这些函数。解构声明其实就是按顺序调用它们。
你想想看,这个特性在遍历集合时特别爽:
val users = listOf(
User(1, "张三", "a@b.com", 20),
User(2, "李四", "c@d.com", 22)
)
for ((id, name) in users) {
println("用户$id: $name")
}
只取前两个字段,后面的忽略掉。简洁又直观。
val (id, _, email) = user。
密封类:受限的类层次结构
密封类这个概念,我第一次接触时觉得有点抽象。说白了,它就是一种受限的继承结构——一个密封类的子类只能是有限的几种,而且必须在同一个文件里定义。
为什么需要它?举个例子。假设我们要表示网络请求的状态:
sealed class NetworkResult<out T> {
data class Success<T>(val data: T) : NetworkResult<T>()
data class Error(val message: String, val code: Int = -1) : NetworkResult<Nothing>()
object Loading : NetworkResult<Nothing>()
}
这里NetworkResult只有三种可能:成功、失败、加载中。用密封类的好处是,当你用when表达式处理时,编译器能帮你检查是否覆盖了所有分支:
fun handleResult(result: NetworkResult<String>) {
when (result) {
is NetworkResult.Success -> println("数据: ${result.data}")
is NetworkResult.Error -> println("错误: ${result.message}")
is NetworkResult.Loading -> println("加载中...")
// 不需要else分支,因为所有情况都覆盖了
}
}
如果以后新增了一个子类,编译器会直接报错,提醒你更新when分支。这比用枚举或者抽象类安全多了。
知识体系总览
下面这张图帮你梳理本章的核心内容:
数据类 vs 普通类:一张表说清楚
| 特性 | 数据类 | 普通类 |
|---|---|---|
| 自动生成toString/equals/hashCode | ✅ 是 | ❌ 需要手动实现 |
| 自动生成copy() | ✅ 是 | ❌ 需要手动实现 |
| 解构声明支持 | ✅ 自动支持 | ❌ 需手动实现componentN() |
| 继承其他类 | ❌ 不能继承(但可实现接口) | ✅ 可以 |
| 主构造函数参数要求 | 至少一个参数 | 无要求 |
密封类与枚举类的区别
很多人会问:密封类和枚举类有什么区别?我简单说说:
- 枚举类:每个枚举常量是单例,不能携带不同的数据。适合表示状态本身。
- 密封类:每个子类可以有不同的属性和行为。适合表示带数据的状态。
举个例子,如果只是表示“成功/失败”两种状态,用枚举就够了。但如果成功时要携带数据,失败时要携带错误信息,那就得用密封类。
好了,这一章的内容就到这里。数据类和密封类在实际项目中用得非常多,建议你多写几个例子感受一下。尤其是copy()和解构声明,用习惯了真的回不去。