美文网首页Kotlin 知识
Kotlin中实现对象深拷贝的3种方式

Kotlin中实现对象深拷贝的3种方式

作者: 元亨利贞o | 来源:发表于2022-08-29 11:38 被阅读0次

    〇、Kotlin中的对象拷贝

    Kotlin 的 data class 默认提供了一种对象拷贝的方式 , 即 data class 类会生成 copy() 方法, 用于对象的拷贝, 这个方法类似于 java.lang.Objectclone() 方法 ! 值得注意的是: Kotlin 的 data classcopy() 方法 和 java.lang.Objectclone()方法, 都是浅拷贝.

    经过测试, 发现 copy() / clone() 方法 返回的对象 的属性都会指被拷贝对象的属性 ( 针对引用类型的属性来说) ! 这就是所谓 \color{red}{浅拷贝}

    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 的一些基本概念:
    1. 数据类会将字段定义为private, 然后提供默认的 getter/setter
    2. 数据类自动重写 equals()、hashCode()、toString() 方法
    3. 会提供一个 copy() 方法, 方便对象的复制, 但是这个 copy() 方法只是浅拷贝 , 对于字段都是 基本数据类型和String 的类来说, 这就足够了。如果类中包含了 基本数据类型和String 之外的引用类型 的字段, 那么必须自己实现深拷贝操作 !!!
    4. 会提供 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)深拷贝

    相关文章

      网友评论

        本文标题:Kotlin中实现对象深拷贝的3种方式

        本文链接:https://www.haomeiwen.com/subject/usvhnrtx.html