跳到主要内容

Kotlin 数据类实战:用 data class 告别 JavaBean 的百行模板代码

· 阅读需 6 分钟
重庆 Kotlin User Group

如果你学过 Java,一定对下面这种代码深恶痛绝——一个简单的用户类,要写 getter、setter、equals()、hashCode()、toString(),加上构造函数,轻轻松松上百行:

// Java:一个"简单"的数据类
public class User {
private final int id;
private final String name;
private final int age;

public User(int id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}

// 下面是 30 行你复制粘贴了无数遍的代码...
public int getId() { return id; }
public String getName() { return name; }
public int getAge() { return age; }

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return id == user.id && age == user.age && Objects.equals(name, user.name);
}

@Override
public int hashCode() {
return Objects.hash(id, name, age);
}

@Override
public String toString() {
return "User(id=" + id + ", name=" + name + ", age=" + age + ")";
}
}

而在 Kotlin 里,同样的功能只需要 一行:

data class User(val id: Int, val name: String, val age: Int)

没错,就这一行。equals()、hashCode()、toString()、copy()、解构方法——编译器全自动生成。

编译器帮你做了什么?​

加了一个 data 关键字后,编译器会为主构造函数中的 所有属性 自动生成以下方法:

方法作用
equals() / hashCode()按属性值比较,而非引用地址
toString()输出 User(id=1, name=Alice, age=20) 格式
copy()基于现有对象快速创建新对象,可修改部分属性
component1() ~ componentN()支持解构声明

来验证一下:

data class User(val id: Int, val name: String, val age: Int)

fun main() {
val u1 = User(1, "Alice", 20)
val u2 = User(1, "Alice", 20)

// equals:按内容比较,不是引用比较
println(u1 == u2) // true

// toString:调试时一目了然
println(u1) // User(id=1, name=Alice, age=20)

// hashCode:可以直接当 Map 的 key 或放进 Set
val userSet = setOf(u1, u2)
println(userSet.size) // 1(内容相同,视为同一个元素)
}

对比 Java 里你要手动重写 equals() 和 hashCode() 的痛苦——这就是 data 关键字的魔力。

copy():不可变更新模式的核心​

在实际开发中,我们经常需要"基于一个对象创建另一个对象,只改其中一两个字段"。比如用户修改了昵称,其他信息不变:

data class Profile(val nickname: String, val avatar: String, val level: Int)

fun main() {
val original = Profile("Alice", "default.png", 5)

// 只改昵称,其他保持不变
val updated = original.copy(nickname = "Alice2026")

println(original) // Profile(nickname=Alice, avatar=default.png, level=5)
println(updated) // Profile(nickname=Alice2026, avatar=default.png, level=5)
}

copy() 返回的是一个全新对象,原对象完全不变。这就是函数式编程中常说的 不可变更新模式(Immutable Update Pattern)。

为什么这很重要?因为不可变对象天然线程安全——在协程并发场景下,你不用担心数据被其他协程偷偷修改。配合 StateFlow 使用更是绝配:

// Android ViewModel 中的典型用法
val _uiState = MutableStateFlow(Profile("Alice", "default.png", 5))
val uiState: StateFlow<Profile> = _uiState

fun onNicknameChanged(newName: String) {
// 用 copy 创建新状态,触发 UI 更新
_uiState.value = _uiState.value.copy(nickname = newName)
}

解构声明:一行拆出所有属性​

数据类自动支持解构,让你可以一行代码把所有属性拆成独立变量:

data class Point(val x: Int, val y: Int)

fun main() {
val point = Point(3, 7)
val (x, y) = point // 解构!
println("x=$x, y=$y") // x=3, y=7
}

不需要某个属性?用下划线跳过:

val (_, y) = point // 只取 y

解构在集合操作中特别好用:

data class Student(val name: String, val score: Int)

fun main() {
val students = listOf(
Student("Alice", 95),
Student("Bob", 82),
Student("Charlie", 91)
)

// 在 map/forEach 中直接解构
students.map { (name, score) ->
"$name: ${if (score >= 90) "优秀" else "加油"}"
}.forEach { println(it) }
// 输出:
// Alice: 优秀
// Bob: 加油
// Charlie: 优秀
}

实战:用 data class 建模 API 响应​

把数据类组合起来,可以非常清晰地建模复杂的业务场景:

// 密封类 + 数据类的黄金组合
sealed class ApiResult<out T> {
data class Success<T>(val data: T, val code: Int = 200) : ApiResult<T>()
data class Error(val message: String, val code: Int) : ApiResult<Nothing>()
object Loading : ApiResult<Nothing>()
}

// 嵌套数据类
data class UserListResponse(
val users: List<UserItem>,
val totalCount: Int,
val page: Int
)

data class UserItem(
val id: Int,
val name: String,
val email: String,
val isActive: Boolean = true
)

// 使用
fun handleResult(result: ApiResult<UserListResponse>) {
when (result) {
is ApiResult.Success -> {
val (users, total, page) = result.data // 解构
println("第 $page 页,共 $total 人")
users.forEach { (name, _, _, active) -> // 部分解构
if (active) println(" - $name (在线)")
}
}
is ApiResult.Error -> println("错误 ${result.code}: ${result.message}")
ApiResult.Loading -> println("加载中...")
}
}

注意 UserItem 的 isActive 属性用了默认值——这意味着创建时可以省略:

val user = UserItem(1, "Alice", "alice@example.com")
// isActive 默认为 true,不需要每次都写

两个容易踩的坑​

坑 1:copy() 是浅拷贝

data class Team(val name: String, val members: MutableList<String>)

fun main() {
val team1 = Team("A队", mutableListOf("Alice", "Bob"))
val team2 = team1.copy() // 看起来创建了副本
team2.members.add("Charlie") // 修改 team2 的成员列表

println(team1.members) // [Alice, Bob, Charlie] ← team1 也被改了!
}

copy() 只复制了 members 的引用,两个 Team 共享同一个 MutableList。要深拷贝,需要显式创建新集合:

val team2 = team1.copy(members = team1.members.toMutableList())

坑 2:类体中的属性不参与自动生成

data class User(val id: Int, val name: String) {
var score: Int = 0 // 在类体中声明,不在主构造函数里
}

val u1 = User(1, "Alice").apply { score = 100 }
val u2 = User(1, "Alice").apply { score = 200 }

println(u1 == u2) // true!score 不同但 equals 认为相等

data class 的 equals()/hashCode()/toString() 只看主构造函数中的属性。类体中声明的属性会被完全忽略。所以,尽量把所有数据都放在主构造函数里。

小结​

场景推荐做法
承载数据的类一律用 data class
属性声明优先用 val,保持不可变
修改部分字段用 copy() 创建新对象
提取多个属性用解构声明 val (a, b) = obj
嵌套数据数据类可以互相嵌套,结构清晰

记住一句话:如果你的类主要用来存数据,就加 data。 这一个关键字,能帮你省掉几百行模板代码。


今日小挑战:定义一个 data class Color(val r: Int, val g: Int, val b: Int, val a: Int = 255),然后完成以下操作:

  1. 创建 Color(255, 0, 0) 和 Color(255, 0, 0, 255),验证它们是否相等
  2. 用 copy() 把红色变成半透明(a = 128)
  3. 用解构声明提取 g 和 b 的值,跳过 r 和 a
  4. 思考:如果把 a 的默认值改成 0,第 1 步的结果会变吗?为什么?