Kotlin数据类基础:从定义到实战

数据类,说白了就是专门用来装数据的类。我刚开始用Kotlin时,最爽的就是这个特性。你想想看,Java里写个POJO类,getter、setter、toString、equals、hashCode,一套组合拳下来,代码量直接翻倍。Kotlin一句data class全搞定。

我个人习惯,只要一个类的主要职责是持有数据,就毫不犹豫用data class。别犹豫,别想着「万一以后要加逻辑怎么办」——真到那时候再改也不迟。

data class的定义

语法很简单,在class前面加个data关键字就行。但有个硬性要求:主构造函数至少有一个参数。为什么?因为数据类的核心就是围绕这些参数展开的。

data class User(
    val id: Long,
    val name: String,
    val email: String
)

就这么一行,抵得上Java里几十行代码。我在项目中接手过一个老系统,光User类就写了200多行,全是模板代码。后来重构时改成data class,整个文件清爽得不像话。

核心要点:主构造函数的参数必须用valvar声明。我个人强烈建议用val,保持不可变性。

自动生成的函数

data class会自动生成几个关键函数。咱们一个一个看。

toString()

默认打印所有主构造函数参数的键值对。调试时特别有用。

val user = User(1, "张三", "zhangsan@example.com")
println(user)  
// 输出: User(id=1, name=张三, email=zhangsan@example.com)

我曾经在排查线上问题时,全靠这个输出定位数据异常。要是Java里没重写toString,打印出来就是User@1a2b3c4d,啥也看不出来。

equals() 和 hashCode()

这两个是一对,必须一起重写。data class会基于主构造函数的所有参数生成。

val user1 = User(1, "张三", "zhangsan@example.com")
val user2 = User(1, "张三", "zhangsan@example.com")
println(user1 == user2)  // true,结构相等
println(user1 === user2) // false,引用相等
小提示:Kotlin的==默认调用equals()===才是比较引用。这和Java不一样,刚转过来的同学容易搞混。

这里有个坑:如果类体里定义了其他属性,它们不会参与equals和hashCode的计算。我在项目中遇到过,有人在data class里加了个var temp用于缓存计算结果,结果两个对象明明数据一样,就因为temp不同被判为不相等。排查了半天才发现问题。

避坑指南:我曾经在data class的类体中定义了一个可变列表,用来缓存一些中间数据。结果两个相同ID的对象,因为列表状态不同,equals返回false。后来我改成在类外部维护缓存,data class只保留核心数据。

copy()

这是data class的杀手锏。基于当前对象创建一个新对象,同时可以修改部分属性。

val original = User(1, "张三", "zhangsan@example.com")
val modified = original.copy(name = "李四")
println(modified) // User(id=1, name=李四, email=zhangsan@example.com)

你想想看,如果没有copy,要修改一个不可变对象的某个字段,你得手动new一个,把所有字段都传一遍。有了copy,只传要改的就行。这在函数式编程和状态管理中特别有用。

我习惯在UI层大量使用copy。比如用户修改个人信息,每次修改都通过copy生成新对象,旧状态不变。这样撤销操作、历史记录都很好实现。

数据类的解构声明

解构声明,说白了就是把一个对象拆成多个变量。data class默认支持,按主构造函数参数的顺序来。

val (id, name, email) = user
println("ID: $id, 姓名: $name, 邮箱: $email")

这个特性在遍历集合时特别爽:

val users = listOf(
    User(1, "张三", "a@b.com"),
    User(2, "李四", "c@d.com")
)

for ((id, name) in users) {
    println("$id: $name")
}

不用写user.iduser.name,直接拆开用。代码简洁不少。

注意:解构声明的变量顺序和主构造函数的参数顺序完全一致。如果你只想要其中几个,可以用_占位:val (_, name) = user

我在写API响应解析时经常这么用。后端返回的JSON解析成data class,然后直接解构拿到关键字段,省去一堆getter调用。

实战建议

说了这么多,总结几条我个人的经验:

  • 能用data class就用,别自己手写模板代码
  • 主构造函数参数尽量用val,保持不可变
  • 类体内不要定义参与equals/hashCode的属性,容易出bug
  • copy()是状态管理的好帮手,多用它来创建新状态
  • 解构声明让代码更简洁,但注意顺序别搞错

嗯,数据类的基础就这些。看似简单,但用好了能大幅提升代码质量。下一章咱们聊聊不可变架构设计,看看怎么把这些基础特性组合起来,构建健壮的应用。