一、反射的基本概念
反射就是允许程序在运行时获取到程序代码的结构,其中程包括的程序结构信息:类、接口、方法、属性等语法特性。
反射的常见用途:
- 在运行时获取类的所有属性、方法、内部类等;
- 可调用给定名称及签名的方法或访问指定名称的属性;
- 通过签名信息(字节码中的signature,所以混淆的时候需要
-keepattributes Signature
)获取泛型实参的具体类型; - 访问运行时注解及其信息完成注入或者配置操作。
Kotlin中反射常用的数据结构:
- KType: 描述未擦除的类型或泛型参数,例如
Map<String,Int>
;可通过typeOf或者以下类型获取对应的父类、属性、函数参数等等; - KClass: 描述对象的实际类型,不包含泛型参数,例如 Map,可通过对象,类型名直接获得;
- KProperty: 描述属性,可通过属性引用,属性所在类的 KClass 获取;
- 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反射常用数据结构的使用
获取KClass
和KProperty
:
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)
复制代码