Kotlin之反射—概念学习与案例实践

一、反射的基本概念

反射就是允许程序在运行时获取到程序代码的结构,其中程包括的程序结构信息:类、接口、方法、属性等语法特性。

反射的常见用途:

  1. 在运行时获取类的所有属性、方法、内部类等;
  2. 可调用给定名称及签名的方法或访问指定名称的属性;
  3. 通过签名信息(字节码中的signature,所以混淆的时候需要 -keepattributes Signature)获取泛型实参的具体类型;
  4. 访问运行时注解及其信息完成注入或者配置操作。

Kotlin中反射常用的数据结构:

  1. KType: 描述未擦除的类型或泛型参数,例如Map<String,Int>;可通过typeOf或者以下类型获取对应的父类、属性、函数参数等等;
  2. KClass: 描述对象的实际类型,不包含泛型参数,例如 Map,可通过对象,类型名直接获得;
  3. KProperty: 描述属性,可通过属性引用,属性所在类的 KClass 获取;
  4. KFunction: 描述函数,可通过函数引用,函数所在类的 KClass 获取。

Kotlin反射的使用:

Java反射的库是嵌在JDk中的,无须额外依赖其他库,但是Kotlin是自身做了一套额外的api来实现反射这个功能,这个库需要依赖进来:

implementation "org.jetbrains.kotlin:kotlin-reflect"
复制代码

在Kotlin中是完全可以使用Java的反射的,因为Kotlin和Java是完全兼容的,下面列出两者的优缺点:

Java反射:

  • 优点:无需引入额外依赖,首次使用速度相对较快(这是因为它的信息都在虚拟机内)。
  • 缺点:无法访问Kotlin语法特性,需要对Kotlin生成的字节码足够了解(这是因为Kotlin程序编译完之后也是个Java类,所以在Java反射视角下看到的Kotlin编译的状态和一般Java类无异,这个时候如果要通过反射访问Kotlin类的一些方法属性,必须要知道它编译什么样的字节码)。

Kotlin反射:

  • 优点:支持访问Kotlin几乎所有特性,API设计兼容性更好。
  • 缺点:额外引入了一个Kotlin反射库(这个库2.5M左右,编译后400KB),首次调用会慢一些,这是因为Kotlin的反射信息是写到Metadata这个注解里面的(通过查看字节码可以看到,如下),里面的数据通过Protobuf的数据格式序列化二进制写入,所以首次反射调用有一个获取注解并反序列化的过程)。
@Metadata(
   mv = {1, 6, 0},
   k = 1,
   d1 = {"\u0000\u001a\n\u0002\u0018\u0002\n\u0000\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\u0002\n\u0002\b\u0003\u0018\u0000*\n\b\u0000\u0010\u0001 \u0000*\u00020\u00022\u00020\u0003B\u0005¢\u0006\u0002\u0010\u0004J\u0013\u0010\u0005\u001a\u00020\u00062\u0006\u0010\u0007\u001a\u00028\u0000¢\u0006\u0002\u0010\b¨\u0006\t"},
   d2 = {"Lcom/qisan/kotlinstu/Dustbin;", "T", "Lcom/qisan/kotlinstu/Waste;", "", "()V", "put", "", "t", "(Lcom/qisan/kotlinstu/Waste;)V", "KotlinStu.app"}
)
复制代码

二、Kotlin反射常用数据结构的使用

获取KClassKProperty:

var cls:KClass<String> = String::class

//cls.java 转成Java的Class<String>,cls.java.kotlin又转回KClass
var clsJava:Class<String> = cls.java 

//获取定义在String类里面的属性,返回的是Collections
val property = cls.declaredMemberProperties
println(property.toString())

打印一下的结果:
[val kotlin.String.length: kotlin.Int]
查看String类,它内部就定义了一个属性:
public override val length: Int
复制代码

KClass中还有以下几个获取KFunction的declareFunctions相关函数:

cls.declaredFunctions
cls.declaredMemberFunctions
cls.declaredMemberExtensionFunctions
复制代码

这里要注意的是declaredMemberExtensionFunctions获取的扩展方法不是类本身的扩展方法,而是定义在这个类里面的扩展方法,比如:

class A{
    fun String.hello(){}
}
复制代码

我们通过A::class.declaredMemberExtensionFunctions是可以获取到hello()方法的,但是通过String的declaredMemberExtensionFunctions是无法获取的,因为declaredMemberExtensionFunctions只能拿到你这个类当中的方法,无法拿到自身的扩展方法,自身的扩展方法是编译成静态函数了。

KClass还定义了很多方法,比如:

cls.isAbstract     //是否是抽象类
cls.isCompanion    //是否是伴生对象
cls.nestedClasses  //获取当前类的内部类
cls.objectInstance //如果是object,可以直接通过这个方法获取

还有很多其他的方法就不一一列举的,需要在实践中多体会。
复制代码

获取KType

val mapType = typeOf<Map<String, Int>>()
mapType.arguments.forEach {
    println(it)
}

打印结果:
kotlin.String
kotlin.Int
复制代码

由上面代码可以看出,通过KType可以找到泛型实参。

三、通过反射获取泛型实参

1、获取接口中方法返回的泛型实参类型:

interface Api{
    fun getUsers():List<UserModel>
}

data class UserModel(
    var id: Int,
    var name: String,
)

fun main(){
    Api::class.declaredFunctions.first{ it.name == "getUsers" }
        .returnType.arguments.forEach {
            println(it)
        }

    //和上面写法等价 Api::getUsers函数引用就是一个KFunction
    Api::getUsers.returnType.arguments.forEach {
        println(it)
    }
}

打印结果:
com.qisan.kotlinstu.reflect.UserModel
com.qisan.kotlinstu.reflect.UserModel
复制代码

2、获取父类的泛型实参

abstract class SuperType<T>{
    val typeParamter by lazy{
        //this表示的是子类的实例,因为抽象类不能用于创建实例,只能背当做父类被子类继承
        this::class.supertypes.first().arguments.first().type
        //如果有多个子类继承 比如class SubType2:SubType(){} 这个时候his::class.supertype会空,因为SubType没有泛型实参这时候要使用:
       //this::class.allSupertypes.first().arguments.first().type
    }
}

open class SubType:SuperType<String>()

class SubType2:SubType()

fun main(){
    val subType = SubType()
    subType.typeParamter.let(::println)
}

打印结果:
kotlin.String
复制代码

四、实践案例学习

反射相关概念性的东西并不多,api很多的方法就不再一一举例了,我们更多还是要在实践中加深理解和使用,下面一起分析学习一下Kotlin中文社区负责人Bennyhuo大神写的两个反射实践案例。

4.1、为数据类实现深拷贝DeepCopy

我们知道data class是实现了一个copy方法的,但是这个拷贝是一个浅拷贝,怎么浅拷贝呢?让我们看一下以下的代码:

//定义两个数据类
data class Person(val id: Int, val name: String, val group: Group)
data class Group(val id: Int, val name: String, val location: String)

//通过Person的实例进行copy拷贝
val person = Person(...)
val copyPerson = person.copy()

//这个时候我们比较一下对象引用
if(person === copyPerson) false
if(person.group === copyPerson.group) true

这种情况就是一种浅拷贝
复制代码

很显然,因为(person.group === copyPerson.group)为true,那我们修改person.group中的值时,copyPerson.group的值也会被修改,这种情况在实际开发中可能会存在不可预知的危险。那深拷贝的要解决的问题就是要使(person.group === copyPerson.group)false

首先,我们要为任何一个数据类提供一个深拷贝的方法,那定义泛型的时候它的父类型的肯定是Any,因为Any是所有类型的父类,然后就简单了,我们定义一个继承Any类的扩展方法。

定义数据类

data class Person(val id: Int, val name: String, val group: Group)

data class Group(val id: Int, val name: String, val location: String)
复制代码

添加DeepCopy()方法

fun <T : Any> T.deepCopy(): T {
    if (this::class.isData) { //先判断一下是否数据类
        return this
    }

    //通过反射获取到数据类的构造方法
    return this::class.primaryConstructor!!.let { primaryConstructor -> //显式出来这个lamba表达式的参数primaryConstructor,不写默认是it,这样可以防止嵌套了其他lamba参数错误
        //通过构造器获取构造器里面的参数,并进行一个map变换
        primaryConstructor.parameters.map { parameter ->

            val value = (this::class as KClass<T>).memberProperties.first { it.name == parameter.name } //这里返回的KProperty
                .get(this) //拿到的是相应的属性对应的值
            //如果获取的这个属性的类型是KClass 就判断一下是否是数据类,如果是就继续深拷贝一下 否则就用当前的parameter直接映射赋值
            if ((parameter.type.classifier as? KClass<*>)?.isData == true) {
                parameter to value?.deepCopy()
            } else {
                parameter to value
            }

        }.toMap()//把map变换的list转换成map
            .let { primaryConstructor.callBy(it) } //primaryConstructor是KFunction<T>类型 KFunction<T>里面有callBy方法 只要传一个map,返回调用者的参数类型T
    }
}
复制代码

构建调用:

fun main() {
    val person = Person(
        1001,
        "QiSan",
        Group(
            1024,
            "Kotliner.cn",
            "JiangNanXi"
        )
    )

    val copyPerson = person.copy()
    val deepCopyPerson = person.deepCopy()

    println(person === copyPerson)
    println(person === deepCopyPerson)

    println(person.group === copyPerson.group)
    println(person.group === deepCopyPerson.group)

    println(deepCopyPerson)
}
复制代码

打印结果: false true true true Person(id=1001, name=QiSan, group=Group(id=1024, name=Kotliner.cn, location=JiangNanXi))

至此,深拷贝就算是实现了。

4.2、Model映射

在我们实际开发的场景中,当两个数据model里面的属性名字是一样的或者部分一样,那我要把一个model的数据赋给另外一个model只能是每个属性对应赋值,对于数据量多的情况这样做是比较麻烦的,而model映射要解决的问题就是数据model通过要反射直接生成一个符合需求model数据类。

声明两个符合可以model映射的数据类:

data class UserVO(val login: String, val avatarUrl: String)

data class UserDTO(
    var id: Int,
    var login: String,
    var avatarUrl: String,
    var url: String,
    var htmlUrl: String
)
复制代码

可以看到UserDTO和UserVO共有的属性是login、avatarUrl,那我们可以通过UserDTO映射得到UserVO。

还要一种情况就是我们可以把map转成适合的数据model对象,例如下面这种情况:

val userMap = mapOf(
    "id" to 0,
    "login" to "Bennyhuo",
    "avatarUrl" to "https://api.github.com/users/bennyhuo",
    "url" to "https://api.github.com/users/bennyhuo"
)
复制代码

所以可以定义两个方法进行映射,一种是用使用map来映射成model,一种是通过model来映射获得新的model,看下面两个方法:

//
inline fun <reified From : Any, reified To : Any> From.mapAs(): To {
    //这里先把已有数据的model转成map,再调用map类型的mapAs转换
    return From::class.memberProperties.map { it.name to it.get(this) }
        .toMap().mapAs()
}

inline fun <reified To : Any> Map<String, Any?>.mapAs(): To {
    return To::class.primaryConstructor!!.let {
        it.parameters.map {
            parameter ->
            //这里是this[parameter.name]是调用的map,如果是空就判断是否可空,可空返回null,不可空抛异常
            parameter to (this[parameter.name] ?: if(parameter.type.isMarkedNullable) null
            else throw IllegalArgumentException("${parameter.name} is required but missing."))
        }.toMap()
            .let(it::callBy)
    }
}
复制代码

这里看出其实跟上一个DeepCopy案例的很相似,这个model映射其实更简单,通过反射获取构造器进行了map变换再构造对象。

fun main() {
    val userDTO = UserDTO(
        0,
        "QiSan",
        "https://p3-passport.byteacctimg.com/img/user-avatar/27af052325f5dc48aac3bc578aeb3e8e~300x300.image",
        "https://juejin.cn/user/1820446987653816",
        "https://juejin.cn/user/1820446987653816"
    )

    val userVO: UserVO = userDTO.mapAs()
    println(userVO)

    val userMap = mapOf(
        "id" to 0,
        "login" to "QiSan",
        "avatarUrl" to "https://p3-passport.byteacctimg.com/img/user-avatar/27af052325f5dc48aac3bc578aeb3e8e~300x300.image",
        "url" to "https://juejin.cn/user/1820446987653816"
    )

    val userVOFromMap: UserVO = userMap.mapAs()
    println(userVOFromMap)
}

打印结果:
UserVO(login=QiSan, avatarUrl=https://p3-passport.byteacctimg.com/img/user-avatar/27af052325f5dc48aac3bc578aeb3e8e~300x300.image)
UserVO(login=QiSan, avatarUrl=https://p3-passport.byteacctimg.com/img/user-avatar/27af052325f5dc48aac3bc578aeb3e8e~300x300.image)
复制代码

Supongo que te gusta

Origin juejin.im/post/7074144076504760350
Recomendado
Clasificación