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.directormovie.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 上查看。



原始标题:Create a Deep Copy of a Kotlin Data Class