3. Kotlin反射实战:运行时获取类信息、调用方法、访问属性、创建实例
反射这个东西,说实话,很多Android开发者对它又爱又怕。爱的是它能在运行时搞定编译期搞不定的事,怕的是用不好容易翻车。我最早接触反射是在做插件化框架的时候,那时候Kotlin还没普及,Java的反射写起来那叫一个啰嗦。后来Kotlin原生支持了反射,语法简洁了不少,但核心思想没变——在运行时探查和操作类的内部结构。
这一节,咱们就扎进去看看Kotlin反射到底怎么用。我会结合自己踩过的坑,把获取类信息、调用方法、访问属性、创建实例这几个核心操作讲透。
3.1 获取类信息:KClass 是入口
在Java里,反射的入口是 Class 对象。Kotlin换成了 KClass。怎么拿到它?三种方式:
- 从实例获取:
obj::class - 从类名获取:
MyClass::class - 从Java Class转换:
javaClass.kotlin
// 方式一:实例引用
val user = User("张三", 28)
val kClass1 = user::class
// 方式二:直接引用类
val kClass2 = User::class
// 方式三:Java互操作
val kClass3 = User::class.java.kotlin
println("类名: ${kClass1.simpleName}") // User
println("包名: ${kClass1.qualifiedName}") // com.example.User
println("是否是数据类: ${kClass1.isData}") // true
println("构造器数量: ${kClass1.constructors.size}")
::class 而不是 .javaClass.kotlin。后者多了一次转换,性能上虽然差别不大,但代码看着不够Kotlin。
拿到 KClass 之后,你能获取的信息非常丰富:
| 属性 | 说明 | 示例值 |
|---|---|---|
| simpleName | 类名(不含包名) | User |
| qualifiedName | 完整限定名 | com.example.User |
| members | 所有成员(属性+方法) | List<KCallable> |
| declaredMembers | 当前类声明的成员(不含继承) | List<KCallable> |
| primaryConstructor | 主构造器 | KFunction? |
| constructors | 所有构造器 | List<KFunction> |
| supertypes | 父类型列表 | List<KType> |
| annotations | 类上的注解 | List<Annotation> |
⚠️ 注意:members 和 declaredMembers 的区别。前者包含从父类继承来的成员,后者只包含当前类自己定义的。我在做序列化框架时,就因为这个区别排查了半天——用 members 拿到了父类的字段,导致序列化结果多出了不该有的数据。
3.2 调用方法:KFunction 与 call
获取方法信息,最常用的是 declaredFunctions 和 functions。然后通过 call 来执行。
data class User(val name: String, var age: Int) {
fun greet(): String = "你好,我是$name"
fun updateAge(newAge: Int) {
age = newAge
}
}
fun main() {
val user = User("李四", 25)
val kClass = user::class
// 获取所有函数
val functions = kClass.declaredFunctions
functions.forEach { func ->
println("方法名: ${func.name}, 参数数: ${func.parameters.size}")
}
// 调用无参方法
val greetFunc = kClass.declaredFunctions.first { it.name == "greet" }
val result = greetFunc.call(user) // 第一个参数是接收者实例
println(result) // 你好,我是李四
// 调用有参方法
val updateFunc = kClass.declaredFunctions.first { it.name == "updateAge" }
updateFunc.call(user, 30)
println(user.age) // 30
}
这里有个坑——call 的第一个参数是接收者实例。对于扩展函数或者顶层函数,接收者可能为 null。我曾经在写一个依赖注入框架时,忘了传接收者,结果抛了个 IllegalArgumentException,提示“Callable expects 1 argument, but 0 provided”。排查了半天才发现是接收者没传。
🚨 我曾经踩过的坑:call 方法不检查参数类型。如果你传了错误类型的参数,它会在运行时才报错。所以调用前最好用 parameters 检查一下参数类型。我后来封装了一个安全调用工具,先比对参数类型再执行,虽然多了一步判断,但稳了很多。
3.3 访问属性:KProperty 与 KMutableProperty
属性反射比方法反射更常用。Kotlin把属性分成了只读(val)和可变(var)两类,分别对应 KProperty 和 KMutableProperty。
fun main() {
val user = User("王五", 22)
val kClass = user::class
// 获取所有属性
val properties = kClass.declaredMemberProperties
properties.forEach { prop ->
println("属性名: ${prop.name}, 类型: ${prop.returnType}")
}
// 读取属性值
val nameProp = kClass.declaredMemberProperties
.first { it.name == "name" }
val nameValue = nameProp.getter.call(user)
println("name = $nameValue") // name = 王五
// 修改可变属性
val ageProp = kClass.declaredMemberProperties
.first { it.name == "age" } as KMutableProperty<*>
ageProp.setter.call(user, 35)
println("age = ${user.age}") // age = 35
}
你可能会问:为什么不直接用 user.age = 35?非要绕一圈用反射?
嗯,正常情况下确实没必要。但如果你在做通用框架,比如对象映射器、JSON序列化器、数据库ORM,你根本不知道用户会传什么对象进来。这时候反射就是唯一的选择。
💡 性能优化建议:反射调用比直接调用慢一个数量级。如果你在循环里频繁调用,可以考虑缓存 KProperty 或 KFunction 对象。我在做数据库批量插入时,把字段的 setter 缓存到 Map 里,性能提升了将近3倍。
3.4 创建实例:构造器反射
创建实例有两种方式:通过 primaryConstructor 或者遍历 constructors。
fun main() {
val kClass = User::class
// 方式一:使用主构造器
val primaryCtor = kClass.primaryConstructor
if (primaryCtor != null) {
val user1 = primaryCtor.call("赵六", 40)
println(user1) // User(name=赵六, age=40)
}
// 方式二:遍历所有构造器
val ctors = kClass.constructors
ctors.forEach { ctor ->
val params = ctor.parameters
println("构造器参数: ${params.map { it.name to it.type }}")
}
// 方式三:无参构造器(如果存在)
val noArgCtor = kClass.constructors.firstOrNull {
it.parameters.isEmpty()
}
if (noArgCtor != null) {
val user2 = noArgCtor.call()
println(user2)
}
}
这里有个细节——call 的参数顺序必须和构造器参数声明顺序一致。Kotlin的构造器参数顺序就是你在类里写的顺序。如果你不确定,可以用 parameters 列表来确认。
⚠️ 注意:如果构造器参数有默认值,call 不会自动填充默认值。你必须显式传参。否则会报参数数量不匹配的错误。我建议用 callBy 方法,它可以按参数名传值,更安全。
// 使用 callBy 按参数名传值
val user3 = primaryCtor?.callBy(mapOf(
primaryCtor.parameters[0] to "孙七",
primaryCtor.parameters[1] to 33
))
println(user3) // User(name=孙七, age=33)
3.5 实战:一个通用的对象拷贝工具
说了这么多理论,咱们来写个实际能用的东西。我经常需要在两个对象之间拷贝属性,比如从DTO转成Entity。手写setter太累,用反射做个通用工具:
fun <T : Any, R : Any> copyProperties(source: T, target: R) {
val sourceClass = source::class
val targetClass = target::class
// 获取源对象的所有可变属性
val sourceProps = sourceClass.declaredMemberProperties
.filterIsInstance<KMutableProperty<*>>()
// 获取目标对象的所有可变属性
val targetProps = targetClass.declaredMemberProperties
.filterIsInstance<KMutableProperty<*>>()
// 按属性名匹配并拷贝
sourceProps.forEach { sourceProp ->
val targetProp = targetProps.firstOrNull {
it.name == sourceProp.name
}
if (targetProp != null) {
val value = sourceProp.getter.call(source)
targetProp.setter.call(target, value)
}
}
}
// 使用示例
data class UserDTO(val name: String, val age: Int)
data class UserEntity(var name: String = "", var age: Int = 0)
fun main() {
val dto = UserDTO("测试用户", 28)
val entity = UserEntity()
copyProperties(dto, entity)
println(entity) // UserEntity(name=测试用户, age=28)
}
这个工具虽然简单,但我在好几个项目里都用到了。当然,生产环境建议用成熟的库(比如MapStruct),但理解背后的原理很重要——说白了,这些库底层也是这么干的。
3.6 知识体系总览
下面这张图把Kotlin反射的核心脉络梳理了一下,你可以对照着回顾:
这张图把反射的四个核心操作串起来了。你从 KClass 出发,可以往三个方向走:获取类信息、调用方法、访问属性。而创建实例则是独立的一条线,通过构造器反射来实现。
说实话,反射这东西,平时用得不多,但一旦需要,就是救命稻草。我个人建议你把这几个API记熟,尤其是 call 和 callBy 的区别、members 和 declaredMembers 的差异。真到用的时候,能省不少排查时间。