Kotlin中实现对象深拷贝的3种方式
〇、Kotlin中的对象拷贝
Kotlin 的 data class
默认提供了一种对象拷贝的方式 , 即 data class
类会生成 copy()
方法, 用于对象的拷贝, 这个方法类似于 java.lang.Object
的 clone()
方法 ! 值得注意的是: Kotlin 的 data class
的 copy()
方法 和 java.lang.Object
的clone()
方法, 都是浅拷贝.
经过测试, 发现 copy() / clone()
方法 返回的对象 的属性都会指被拷贝对象的属性 ( 针对引用类型的属性来说) ! 这就是所谓
tips: kotlin用 ===
判断引用是否相同, Java用 ==
判断引用是否相同
下面是对 clone() 和 copy()
方法的测试:
// Kotlin: (objCopy 是obj.copy() 返回的对象)
obj === objCopy // false
obj.fieldA === objCopy.fieldA // true (fieldA是除了基本类型和String 之外的其他引用类型)
// Java: (objCopy 是 obj.clone() 返回的对象)
obj == objCopy // false
obj.fieldA == objCopy.fieldA // true
Kotlin 的 data class
的一些基本概念:
- 数据类会将字段定义为private, 然后提供默认的 getter/setter
- 数据类自动重写 equals()、hashCode()、toString() 方法
- 会提供一个 copy() 方法, 方便对象的复制, 但是这个 copy() 方法只是浅拷贝 , 对于字段都是 基本数据类型和String 的类来说, 这就足够了。如果类中包含了 基本数据类型和String 之外的引用类型 的字段, 那么必须自己实现深拷贝操作 !!!
- 会提供 component#() 方法 (# 为字段编号, 编号从1开始), component# 方法主要用于 解构赋值
如:val (name,age,sex) = personObject
定义一个 data class
// 数据类会重写: equals, hashCode, toString 这三个方法 并 自动生成 getter/setter
data class Person(var name:String, var age:Int, var sex:Boolean)
编译数据类Person后, 可以使用下列命令来查看字节码文件的内容:
$ javap -p -c com.stone.demo.basic.Person
-p 输出private信息
-c 反编译字节码内容
下面是使用javap反编译一个 data class
类的字节码文件的输出内容, 由于方法的指令部分的内容太多, 已经被删除了, 只留着方法签名部分:
// 注意: 需要先编译出Person.class 文件, 才能使用 javap 反编译:
// javap -p -c com.stone.demo.basic.Person
// 输出内容如下:
public final class com.stone.demo.basic.Person {
// 1. 定义类3个字段:
private java.lang.String name;
private int age;
private boolean sex;
// 2. primary 构造函数:
public com.stone.demo.basic.Person(java.lang.String, int, boolean);
// 3. 字段对应的 getter/setter
public final java.lang.String getName();
public final void setName(java.lang.String);
public final int getAge();
public final void setAge(int);
public final boolean getSex();
public final void setSex(boolean);
// 4. 每个字段都生成一个对应的 componen#() 方法 --- # 为字段的编号 , # 从 1 开始
public final java.lang.String component1();
public final int component2();
public final boolean component3();
// 5. copy() 方法: 实例方法copy() 和 静态方法copy$default()
public final com.stone.demo.basic.Person copy(java.lang.String, int, boolean);
public static com.stone.demo.basic.Person copy$default(com.stone.demo.basic.Person, java.lang.String, int, boolean, int, java.lang.Object);
// 6. 重写 toString, equals, hashCode 方法
public java.lang.String toString();
// 重写hashCode方法
public int hashCode();
// 重写 equals() 方法
public boolean equals(java.lang.Object);
}
Kotlin中的基本数据类型 (kotlin中这些数据也都是引用类型的, 分别对应于Java的包装类):
Boolean, Byte, Char, Short, Int, Long, Float, Double,
另外 String 会使用常量池 , 深拷贝不考虑它 !
一、Kotlin中深拷贝的实现方式一
DeepCopy.kt 文件 , 内容如下:
package com.stone.demo.basic.deepcopy1
class DeepCopy {
}
interface DeepCopyable<out R> {
fun deepCopy(): R
}
// 一. kotlin实现对象的深度拷贝
// 实现方式: 使用 直接 "创建对象" 的方式实现深度拷贝
// 优点: 可以用于 "data-class" 和 "非data-class"
// 缺点: a. 需要实现接口并实现接口中的方法, 使用麻烦
// b. 对于属性层级太深的类型, 实现起来会非常麻烦, 而且有可能导致某些属性无法实现深层拷贝
data class Person(var name:String, var age:Int, var sex: Boolean)
data class Panda (var name:String, var owner:Person) : DeepCopyable<Panda> {
override fun deepCopy(): Panda {
return Panda(name, Person(owner.name, owner.age, owner.sex))
}
}
class A(var name:String)
class B(var field: A) : DeepCopyable<B> {
override fun deepCopy(): B {
// 数字, 整型数据类型都有一个缓存 [-128, 127], 拷贝前后的属性对象会指向同一个
// 字符串常量, 会放置在常量池中, 拷贝前后的属性对象也会指向同一个
// return B(A(field.name.copy)) // 基本类型 和 String 不需要 拷贝 !!
return B(A(field.name))
}
}
fun main() {
// 0x1: 对于普通类
val b = B(A("哈哈哈"))
val bCopy = b.deepCopy()
println(b === bCopy) // false
println(b.field === bCopy.field) // false
// 0x2. 对于数据类:
val panda = Panda("团团", Person("张三", 22, true))
val pandaCopy = panda.copy()
val pandaDeepCopy = panda.deepCopy()
println("===================== for copy")
println(panda === pandaCopy) // false
println(panda === pandaDeepCopy) // false
println("===================== for owner-field of copy-object")
println(panda.owner === pandaCopy.owner) // true (浅拷贝, 拷贝后的对象的属性还是会执行源对象的属性)
println(panda.owner === pandaDeepCopy.owner) // false
}
二、Kotlin中深拷贝的实现方式二
DeepCopy.kt 文件 , 内容如下:
package com.stone.demo.basic.deepcopy2
import kotlin.reflect.KClass
import kotlin.reflect.full.memberProperties
import kotlin.reflect.full.primaryConstructor
class DeepCopy {
}
// 二. 实现深拷贝的方式2 (成员的类型 必须是 数据类 才能实现深度拷贝)
// https://cloud.tencent.com/developer/article/1941174
// 实现: 使用扩展方法 并 通过反射的方式 实现深拷贝
// 优点: 使用简单
// 缺点: 这种方式的深拷贝只对 "data class" 有效, 对于 "非 data class" 则不支 (还是浅拷贝)
fun <T: Any> T.deepCopy():T {
if(!this::class.isData) { // 不是数据类, 返回对象本身 ( "拷贝" 后的对象指向原对象)
return this
}
// 下面则一长串的逻辑是: 通过反射收集创建 "copy对象" 所需要的参数, 然后通过反射调用primary构造方法创建 "copy对象"
// 关键点: data class 的primary构造方法的参数是可以通过反射获取到参数名的 且 参数名和类的字段名保持一一对应的关系
// 通过上面这点, 可以匹配并获得字段 (primary构造方法的参数的运行时值) 的具体值, 然后构造一个参数列表, 最后调用primary构造方法
// kotlin.reflect.KFunction<out R> => 构造函数的Kotlin反射类型
// 拿到primary构造函数 , 然后在primary构造函数上做一些操作, 最后使用此构造函数创建对象 (参数由 this 经过 反射获取)
return this::class.primaryConstructor!!.let { primaryConstructor -> // primaryConstructor 是个 java.lang.reflect.Constructor 类型的对象
// primaryConstructor.parameters 是个 List<KParameter> 类型的对象, 即构造函数的参数列表
// List的map()函数返回的是一个Iterable对象
// 这里 primaryConstructor.parameters.map() 的返回结果是一个 Iterable<Pair<*,*>> 类型
primaryConstructor.parameters.map { parameter -> // parameter 是一个 KParameter 对象, 即构造函数的某一个参数
// (this::class as KClass<T>).memberProperties 这玩意是某个类成员属性列表(所有的成员) , 类型为 Collection<KProperty1<T, *>>
val value = (this::class as KClass<T>).memberProperties.first {
it.name == parameter.name // 参数名 和 成员属性 的名称相等时, 取得此成员的值 (data class 的 成员字段的名称 和 primary构造函数的参数的名字是一一对应的)
}.get(this)
// 当参数 (也是成员) 的类型是 "数据类" 时, 对参数 (成员) 进行深拷贝 !!
if((parameter.type.classifier as? KClass<*>)?.isData == true) {
// 参数 (成员) 为数据类时, 进行深拷贝
parameter to value?.deepCopy() // 最后一个 (程序的逻辑上的最后一行) 表达式的计算结果作为lambda的返回值时, return 关键字可以省略
} else {
// 非数据类, 直接返回字段的值 (不做拷贝)
parameter to value // 普通类型, 进行浅拷贝
// key to value => 会构造一个Pair对象, Pair对象是Map的元素
// to 函数 位于 kotlin包中, 在Tuples.kt文件中定义, 是一个 infix 函数, 具体定义如下:
// public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)
// infix 函数, 为中缀函数, 可以省略 函数调用符 "." 和 表示函数的 "()"
}
}.toMap().let(primaryConstructor::callBy) // 通过反射收集创建 "copy对象" 所需要的参数, 然后通过反射调用primary构造方法创建 "copy对象"
// Iterable<Pair<*,*>> 类型可以转换为Map对象
}
}
// a. 数据类
data class A(var name:String)
data class B(var field:A)
// b. 非数据类
class C(var name:String)
class D(var field:C)
fun main() {
// 1. 数据类
val b = B(A("哈哈"))
val bCopy = b.deepCopy()
println(b === bCopy) // false
println(b.field === bCopy.field) // false
// 2. 非数据类
val d = D(C("哈哈"))
val dCopy = d.deepCopy()
println(d === dCopy) // true
println(d.field === dCopy.field) // true
}
三、Kotlin中深拷贝的实现方式三
DeepCopy.kt 文件 , 内容如下:
package com.stone.demo.basic.deepcopy3
import kotlin.reflect.KClass
import kotlin.reflect.full.memberProperties
import kotlin.reflect.full.primaryConstructor
class DeepCopy {
}
// 三. DeepCopyable接口 和 数据类 结合的方式 (优先使用DeepCopyable接口的deepCopy()方法进行深拷贝)
// 实现: 反射调用DeepCopyable接口的deepCopy()方法 或者 反射调用 "data class类" 的copy() 方法 进行深拷贝!
// 优点: 可实现深拷贝的对象范围更大 (实现了DeepCopyable接口的对象, "data class类" 的对象)
// 缺点: 对于既没有实现 DeepCopyable 接口 也不是 "data class类" 的对象, 仍然无法进行深拷贝 !!
// 注意:
// 扩展方法名如果能与DeepCopyable接口的方法 同名, 则对象在调用deepCopy时, 会优先调用对象本身定义的方法,
// 而不是调用扩展方法 !!
// 如果要统一调用 扩展方法deepCopy() , 那么需要将 DeepCopyable 接口的方法更改为其他名称,
// 下面的扩展方法需要更改的地方有两点:
// 1. 下面的扩展方法中的步骤1中 查找 DeepCopyable 接口的方法的条件 应更改为: it.name = "DeepCopyable中深拷贝方法名"
// 2. 下面的扩展方法中的步骤2中 参数收集的地方, 如果参数实现了DeepCopyable接口, 则参数的深拷贝应改为: value?.DeepCopyable接口中深拷贝方法() , 如 value?.dc()
fun <T : Any> T.deepCopy(): Any? { // 注意: deepCopy的返回值虽然声明为Any? , 但是, 其真实类型仍然是 T
// 调用deepCopy() 后 可以对其进行转型
// 如: val a:Type? = typeObject.deepCopy() as? Type
println("扩展方法 deepCopy() 被调用 !")
// 1. 对于实现了 DeepCopyable<R> 接口的类, 直接通过反射调用 deepCopy() 方法进行深拷贝
if(this is DeepCopyable<*>) { // 如果扩展方法 和 DeepCopyable中的方法 同名, 则对象调用深拷贝方法时, 不会调用扩展方法 (这样一来, 这段判断就没有用了 !).
// 因为类本身的深拷贝方法会被优先调用.
// kotlin方式调用有问题: Exception in thread "main" java.lang.IllegalArgumentException: Callable expects 1 arguments, but 0 were provided.
/*
return this::class.declaredMemberFunctions.first {
it.name == "deepCopy"
}.call()*/
// 使用Java的反射调用:
return this::class.java.declaredMethods.first {
it.name == "deepCopy" // deepCopy 是 DeepCopyable<R> 接口中实现深拷贝的方法
// 如果要同一调用裤子方法来进行深拷贝, 可以将DeepCopyable中的深拷贝方法, 进行更名,
// 如 dc, 那么此处的条件也需要更改为: it.name == "dc"
}.invoke(this)
}
// https://cloud.tencent.com/developer/article/1941174
// 2. 如果是数据类 (data class) , 则通过反射调用数据类的copy()方法进行深拷贝
// 注意: 需要考虑对象的字段的类型, 字段的类型分为3种:
// 1. 实现了 DeepCopyable<R> 接口的字段
// 2. data class
// 3. 非上述两类 (这一大类分为两种情况: a. 基本数据类型和String --- 这种属性拷贝是没有问题的
// b. 普通的引用类型 (既没有实现DeepCopyable接口, 也不是data class) --- 这种属性只能实现浅拷贝了 )
if(this::class.isData) {
// copy()方法的的参数列表 和 primary构造方法的 参数列表是一样的,
// 但是copy()方法的参数列表的参数是无法获取到参数名称的, primary构造方法的参数是可以获取到参数名的, 且参数名就是类的字段名称
/*
// data class 的 copy() 方法 (这种方式找到的copy方法, 是kotlin版本的copy, 而调用时调用的字节码中的copy方法, 字节码中的copy方法是java版本的copy方法)
val dataClassCopyMethod = this::class.declaredMemberFunctions.first { copyMethod ->
copyMethod.name == "copy"
}
// 字节码中的方法签名是: copy(java.lang.String)
// 获取到的方法前面却是: copy(kotlin.String)
println("dataClassCopyMethod => $dataClassCopyMethod") // fun com.stone.demo.basic.A.copy(kotlin.String): com.stone.demo.basic.A
// dataClassCopyMethod 的 类型是 KFunction<*>, 如何将 KFunction<*> 转换成 Java的java.lang.reflect.Method ??
// 答案是, 直接找java版本的copy方法, 而不是找kotlin版本的copy方法
*/
// 查找copy()方法, 使用Java版本的copy()方法
val dataClassCopyMethod = this::class.java.declaredMethods.first { copyMethod ->
copyMethod.name == "copy" // copy 是 数据类中提供的copy()方法
}
// println("dataClassCopyMethod => $dataClassCopyMethod") // copy(kotlin.String): com.stone.demo.basic.A
// data class 的 primary构造方法
val primaryConstructor = this::class.primaryConstructor
/*
// 使用primary构造方法的参数类收集copy()方法的运行时参数, 而不是使用copy()方法的参数来收集copy方法的运行时参数.
// 原因是: copy() 方法的参数名是获取不到的, primary构造方法的参数数名称是可以获取到的, 且参数名就是字段的名称
return dataClassCopyMethod.parameters.map { parameter ->
// java.util.NoSuchElementException: Collection contains no element matching the predicate.
val value = (this::class as KClass<T>).memberProperties.first {
println("it.name=${it.name} , parameter.name=${parameter.name} , parameter.type=${parameter.type} , it.javaClass.typeName=${it.javaClass.typeName}")
// parameter.name 的值 是 null => copy() 方法的参数名是获取不到的, primary构造方法的参数数名称是可以获取到的, 且参数名就是字段的名称
it.name == parameter.name
}.get(this)
parameter to value?.deepCopy1()
}.toMap().let( dataClassCopyMethod::callBy )
*/
/*
return primaryConstructor?.parameters?.map { parameter ->
// java.util.NoSuchElementException: Collection contains no element matching the predicate.
val value = (this::class as KClass<T>).memberProperties.first {
println("it.name=${it.name} , parameter.name=${parameter.name} , parameter.type=${parameter.type} , it.javaClass.typeName=${it.javaClass.typeName}")
it.name == parameter.name
}.get(this)
if(value is DeepCopyable1<*>) {
parameter to value?.deepCopy()
} else if(value != null && value::class.isData) { // 如果是数据类, 就递归调用 deepCopy1()
println("value => $value")
// java.lang.IllegalArgumentException: No argument provided for a required parameter: instance parameter of fun com.stone.demo.basic.A.copy(kotlin.String): com.stone.demo.basic.A
// data class A(val name: String) 类的copy()方法, 编译后的签名是: copy(java.lang.String) , 而不是 copy(kotlin.String)
parameter to value.deepCopy1()
} else {
// 此处再做一下处理, 否则出现: // java.lang.IllegalArgumentException: No argument provided for a required parameter: instance parameter of fun com.stone.demo.basic.A.copy(kotlin.String): com.stone.demo.basic.A
if(value is String) {
parameter to java.lang.String(value)
} else {
parameter to value
}
// Exception in thread "main" java.lang.IllegalArgumentException:
// No argument provided for a required parameter: instance parameter of fun com.stone.demo.basic.A.copy(kotlin.String): com.stone.demo.basic.A
}
}?.toMap()?.let( dataClassCopyMethod::callBy ) // kotlin 反射方法的调用方式
*/
val params = primaryConstructor?.parameters?.map { parameter ->
val value = (this::class as KClass<T>).memberProperties.first {
// println("it.name=${it.name} , parameter.name=${parameter.name} , parameter.type=${parameter.type} , it.javaClass.typeName=${it.javaClass.typeName}")
it.name == parameter.name
}.get(this)
// 数据类的字段分为三种:
// 1. 实现了 DeepCopyable<R> 接口的字段
// 2. data class
// 3. 非上述两类 (这一大类分为两种情况:
// a. 基本数据类型和String --- 这种属性拷贝是没有问题的
// b. 普通的引用类型 (既没有实现DeepCopyable接口, 也不是data class) --- 这种属性只能实现浅拷贝了
//
// 对于三种类型的字段分别实现相对于的参数收集逻辑 !!
if(value is DeepCopyable<*>) {
value?.deepCopy() // 如果 要同一使用 扩展方法来进行深拷贝, 则 DeepCopyable 方法需要更名为其他名字,
// 且此处改为: value?.DeepCopyable接口中进行深拷贝的方法()
// 例如: DeepCopyable 接口中深拷贝方法为 dc(), 则此处 应为: value?.dc()
} else if(value != null && value::class.isData) { // 如果是数据类, 就递归调用 deepCopy()
// println("value => $value")
// java.lang.IllegalArgumentException: No argument provided for a required parameter: instance parameter of fun com.stone.demo.basic.A.copy(kotlin.String): com.stone.demo.basic.A
// data class A(val name: String) 类的copy()方法, 编译后的签名是: copy(java.lang.String) , 而不是 copy(kotlin.String)
value.deepCopy() // 这里value.deepCopy() 是递归调用当前 (扩展) 方法
} else {
value // 返回属性本身 (浅拷贝)
// Exception in thread "main" java.lang.IllegalArgumentException:
// No argument provided for a required parameter: instance parameter of fun com.stone.demo.basic.A.copy(kotlin.String): com.stone.demo.basic.A
}
}?.toTypedArray()
// 注意: return 不能少, 否则会 调用最后的 "return this"
return if(params == null) {
dataClassCopyMethod.invoke(this)
} else {
// https://blog.csdn.net/xlh1191860939/article/details/82109086
// 将 Array<T> 转换为可变参数 *arrayVar
dataClassCopyMethod.invoke(this, *params)
}
}
// 3. 既没有实现 DeepCopyable<R> 接口, 也不是 "data class 类, 直接返回对象本身 (浅拷贝 --- 引用拷贝 --- 只是在栈上拷贝了一个引用,这个引用仍然指向源对象 !)
return this
}
// a. 实现了 DeepCopyable 接口的类
interface DeepCopyable<out R> {
fun deepCopy(): R
}
data class Person(var name:String, var age:Int, var sex: Boolean)
data class Panda (var name:String, var owner:Person) : DeepCopyable<Panda> {
override fun deepCopy(): Panda {
return Panda(name, Person(owner.name, owner.age, owner.sex))
}
}
// b. 数据类
data class A(var name:String)
data class B(var field:A)
// c. 非数据类
class C(var name:String)
class D(var field:C)
// 测试代码:
fun main() {
// 1. 实现了 DeepCopyable 接口的类的对象
val panda = Panda("圆圆", Person("张三", 32, true))
val pandaDeepCopy = panda.deepCopy() // 此处实际上是调用 DeepCopyable 的 deepCopy()方法, 而不是调用扩展方法 deepCopy()
// 由输出信息可以看出 !
// 扩展函数中的 println("扩展方法 deepCopy() 被调用 !") 此句代码并未被调用 !
println("======================= test for DeepCopyable")
println(panda === pandaDeepCopy) // false
println(panda.owner === pandaDeepCopy.owner) // false
// 2. data class 类 的对象
println("======================= test for data class")
val b = B(A("哈哈"))
val bCopy = b.deepCopy() as B
println(b === bCopy) // false
println(b.field === bCopy.field) // false
// 3. 非上述两类对象
println("======================= test for other")
val d = D(C("嘻嘻"))
val dCopy = d.deepCopy() as D
println(d === dCopy) // true
println(d.field === dCopy.field) // true
}
References:Kotlin | 实现数据类(data)深拷贝