1. 简介
在 Kotlin 开发中,数据类(data class)是我们日常编码中非常常见的一种类结构,主要用于保存数据。当我们需要复制一个数据类实例时,通常使用 Kotlin 提供的 copy()
方法即可。但 这种复制本质上是浅拷贝(shallow copy),对于嵌套对象结构,复制后的对象仍然会引用原始对象中的子对象。
本篇文章将带你了解如何在 Kotlin 中实现数据类的 深拷贝(deep copy),确保复制后的对象完全独立于原对象,即使修改原始对象的嵌套属性也不会影响拷贝对象。
2. 浅拷贝(Shallow Copy)
我们先来看一个简单的数据类结构:
data class Person(var firstName: String, var lastName: String)
data class Movie(var title: String, var year: Int, var director: Person)
2.1. 简单对象
对于 Person
这样只包含基本类型属性的类,使用默认的 copy()
方法就足够了:
@Test
fun givenSimpleObject_whenCopyCalled_thenStructurallyEqualAndDifferentReference() {
val person = Person("First name", "Last name")
val personCopy = person.copy()
assertNotSame(person, personCopy)
assertEquals(person, personCopy)
}
✅ 由于 Person
不包含嵌套对象,此时浅拷贝等价于深拷贝。
2.2. 复杂对象
当对象中包含嵌套对象时,问题就出现了:
@Test
fun givenComplexObject_whenCopyCalled_thenEqualAndDifferentReferenceAndEqualInternalReferences() {
val movie = Movie("Avatar 2", 2022, Person("James", "Cameron"))
val movieCopy = movie.copy()
assertNotSame(movieCopy, movie)
assertEquals(movieCopy, movie)
assertSame(movieCopy.director, movie.director) // 引用相同
movie.director.lastName = "Brown"
assertEquals(movieCopy.director.lastName, movie.director.lastName)
}
❌ 此时 movieCopy.director
和 movie.director
是同一个对象,修改其中一个会影响另一个。
2.3. 防止浅拷贝带来的副作用
将 lastName
改为 val
可以防止误改属性,但 director 的引用仍然一致。同理,如果属性是 MutableList
而非 List
,也会面临同样的问题。
3. 深拷贝(Deep Copy)
为了解决浅拷贝的问题,我们需要手动实现深拷贝逻辑,确保每个嵌套对象也被重新创建。
3.1. 使用 copy()
并自定义参数
Kotlin 的 copy()
方法允许我们传入自定义参数,我们可以借此实现深拷贝:
val movieCopy = movie.copy(director = movie.director.copy())
✅ 这样 director
属性会重新创建一个新的对象实例。
@Test
fun givenComplexObject_whenCustomCopyCalled_thenEqualAndNewReferenceAndDifferentInternalReferences() {
val movie = Movie("Avatar 2", 2022, Person("James", "Cameron"))
val movieCopy = movie.copy(director = movie.director.copy())
assertNotSame(movieCopy, movie)
assertEquals(movieCopy, movie)
assertNotSame(movieCopy.director, movie.director)
movie.director.lastName = "Brown"
assertNotEquals(movieCopy.director.lastName, movie.director.lastName)
}
⚠️ 缺点是如果嵌套层级较深,需要手动一层层 copy,维护起来略显繁琐。
3.2. 实现 Cloneable
接口
Kotlin 支持 Java 的 Cloneable
接口,可以借此实现深拷贝:
data class Person(var firstName: String, var lastName: String) : Cloneable {
public override fun clone(): Person = super.clone() as Person
}
对于嵌套类,我们需要手动 clone 子对象:
data class Movie(var title: String, var year: Int, var director: Person) : Cloneable {
public override fun clone() = Movie(title, year, director.clone())
}
使用方式:
val movieCopy = movie.clone()
✅ 这种方式适合对象结构相对固定、嵌套层级不深的场景。
⚠️ 缺点是需要为每个类都实现 clone 方法,代码重复度高。
3.3. 使用 JSON 序列化进行深拷贝
如果你希望一劳永逸地实现深拷贝,可以借助 JSON 序列化工具。原理是:将对象序列化为 JSON,再反序列化回来,自动创建新的对象实例。
支持的库包括:
示例使用 Gson:
val gson = Gson()
val movieCopy = gson.fromJson(gson.toJson(movie), Movie::class.java)
✅ 优点是无需手动编写拷贝逻辑,适合复杂嵌套结构。
⚠️ 缺点是性能略差,且需要引入额外库。
4. 总结
方法 | 是否深拷贝 | 优点 | 缺点 |
---|---|---|---|
默认 copy() |
❌ 否 | 简洁、Kotlin 原生支持 | 仅浅拷贝,嵌套对象引用相同 |
自定义 copy() |
✅ 是 | 灵活、可控 | 需要手动处理嵌套结构 |
Cloneable 接口 |
✅ 是 | 易于集成 | 每个类都要实现 clone |
JSON 序列化 | ✅ 是 | 通用性强、一劳永逸 | 性能略低,需引入库 |
📌 推荐策略:
- 对于结构简单、嵌套少的对象,优先使用 自定义
copy()
- 对于结构复杂、嵌套深的对象,建议使用 JSON 序列化
Cloneable
是一种备选方案,适合已有 Java 项目迁移
完整示例代码可在 GitHub 上查看。