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}")
💡 我的习惯:在Android项目里,我倾向于用 ::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>

⚠️ 注意membersdeclaredMembers 的区别。前者包含从父类继承来的成员,后者只包含当前类自己定义的。我在做序列化框架时,就因为这个区别排查了半天——用 members 拿到了父类的字段,导致序列化结果多出了不该有的数据。

3.2 调用方法:KFunction 与 call

获取方法信息,最常用的是 declaredFunctionsfunctions。然后通过 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)两类,分别对应 KPropertyKMutableProperty

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,你根本不知道用户会传什么对象进来。这时候反射就是唯一的选择。

💡 性能优化建议:反射调用比直接调用慢一个数量级。如果你在循环里频繁调用,可以考虑缓存 KPropertyKFunction 对象。我在做数据库批量插入时,把字段的 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反射的核心脉络梳理了一下,你可以对照着回顾:

Kotlin反射核心知识体系 KClass 入口 获取类信息 simpleName qualifiedName annotations 调用方法 declaredFunctions call() / callBy() 参数类型检查 访问属性 KProperty / KMutableProperty getter.call() / setter.call() 属性名匹配 创建实例 primaryConstructor.call()

这张图把反射的四个核心操作串起来了。你从 KClass 出发,可以往三个方向走:获取类信息、调用方法、访问属性。而创建实例则是独立的一条线,通过构造器反射来实现。

说实话,反射这东西,平时用得不多,但一旦需要,就是救命稻草。我个人建议你把这几个API记熟,尤其是 callcallBy 的区别、membersdeclaredMembers 的差异。真到用的时候,能省不少排查时间。


公众号:蓝海资料掘金营,微信deep3321