23、反射:反射的概念、类引用与函数引用、获取属性与函数、调用反射

反射,说白了就是让程序在运行时「看穿」自己。你写代码的时候,类型、函数、属性都是固定的。但反射能让你在代码跑起来之后,再去问:「这个对象到底是什么类型?它有哪些方法?能不能帮我调一下?」

我第一次接触反射是在做插件化框架的时候。那时候需要动态加载一个外部模块,调用里面的某个函数。编译期根本不知道那个类长什么样,只能靠反射。嗯,那会儿踩了不少坑,后面慢慢跟大家聊。

反射的概念

反射的核心能力就四个字:运行时自省。Kotlin 的反射和 Java 的反射底层是互通的,但 Kotlin 提供了一套更安全、更符合 Kotlin 语法的 API —— KClassKFunctionKProperty 这些。

你想想看,没有反射的时候,你 new 一个对象、调一个方法,都是编译期写死的。有了反射,你可以用字符串来指定类名、方法名。这带来了灵活性,但也牺牲了一部分性能。

核心概念速览:
  • KClass —— 对应 Java 的 Class,描述一个类的元信息
  • KCallable —— 所有可调用元素的父接口(函数、属性都算)
  • KFunction —— 描述一个函数
  • KProperty —— 描述一个属性(val/var)

我个人习惯把反射分成两个层面:类引用成员操作。先拿到类的「镜子」,再通过镜子去操作里面的东西。

Kotlin 反射核心结构 KClass 类引用 ::class 成员操作 函数引用 ::fun 属性引用 ::prop 获取属性 · 调用函数 运行时自省:编译期未知,运行时可知

类引用与函数引用

1. 类引用:::class

获取一个类的 KClass 非常简单。直接写 MyClass::class 就行。这在 Java 里你得写 MyClass.class,Kotlin 的写法更直观。

// 获取 KClass
val kClass = String::class
println(kClass.simpleName)  // 输出: String

// 从对象实例获取
val str = "hello"
val kClass2 = str::class
println(kClass2.qualifiedName) // 输出: kotlin.String

我在项目中遇到过一个问题:用 Java::classobj.javaClass 有什么区别?::class 返回的是 Kotlin 的 KClass,而 javaClass 返回的是 Java 的 Class。如果你需要和 Java 反射库交互,记得用 .java 转一下。

小技巧:MyClass::class.java 可以快速拿到 Java 的 Class 对象,兼容旧版框架。

2. 函数引用:::fun

函数引用是 Kotlin 反射里很优雅的设计。你可以把函数当作值来传递,用 :: 操作符。

fun isEven(x: Int): Boolean = x % 2 == 0

// 函数引用
val predicate: (Int) -> Boolean = ::isEven
println(predicate(4)) // true

// 绑定到对象
class Calculator {
    fun add(a: Int, b: Int) = a + b
}
val calc = Calculator()
val boundAdd: (Int, Int) -> Int = calc::add
println(boundAdd(3, 5)) // 8

你想想看,函数引用让高阶函数用起来特别顺手。比如 list.filter(::isEven),比写 lambda 更清晰。

3. 属性引用:::prop

属性引用和函数引用类似,但操作的是 KProperty 对象。

data class Person(val name: String, var age: Int)

val nameProp = Person::name
val ageProp = Person::age

val person = Person("Alice", 30)
println(nameProp.get(person)) // Alice
ageProp.set(person, 31)
println(person.age) // 31
注意: val 属性只有 get()var 属性才有 set()。如果你试图对 val 属性调用 set,编译期就会报错。反射不会绕过这个限制。

获取属性与函数

拿到 KClass 之后,就可以枚举它的成员了。这里有两个常用 API:declaredMembersmembers

  • members —— 返回所有公开成员(包括继承来的)
  • declaredMembers —— 只返回当前类声明的成员(不包括继承的)
class User(val id: Int, private var password: String) {
    fun login(pwd: String): Boolean = password == pwd
}

fun inspectUser() {
    val kClass = User::class
    
    // 获取所有函数
    val functions = kClass.members.filterIsInstance<KFunction<*>>()
    println("函数列表:")
    functions.forEach { println("  - ${it.name}") }
    
    // 获取所有属性
    val properties = kClass.members.filterIsInstance<KProperty<*>>()
    println("属性列表:")
    properties.forEach { println("  - ${it.name}") }
}

// 输出:
// 函数列表:
//   - login
//   - equals
//   - hashCode
//   - toString
// 属性列表:
//   - id

注意看,password 属性没有出现在 members 里,因为它是 private 的。要获取私有成员,得用 declaredMemberPropertiesdeclaredMemberFunctions

获取私有成员:
// 获取私有属性
val passwordProp = User::class.declaredMemberProperties
    .first { it.name == "password" }
passwordProp.isAccessible = true  // 绕过访问检查
println(passwordProp.get(User(1, "secret"))) // secret

我曾经在写 ORM 框架的时候,需要读取实体类的私有字段来拼接 SQL。那时候 isAccessible = true 救了我一命。但要注意,这破坏了封装性,生产环境慎用。

调用反射

反射的最终目的就是调用。Kotlin 提供了 call()callBy() 两种方式。

1. call() —— 按参数顺序调用

class MathUtils {
    fun multiply(a: Int, b: Int): Int = a * b
}

val math = MathUtils()
val method = MathUtils::class.members
    .first { it.name == "multiply" } as KFunction<*>

val result = method.call(math, 6, 7)
println(result) // 42

call() 的第一个参数是接收者对象(如果是成员函数的话),后面跟实际参数。参数顺序必须和声明一致。

2. callBy() —— 按参数名调用

如果参数很多,或者你想跳过某些默认参数,callBy() 更合适。它接受一个 Map<KParameter, Any?>

fun greet(name: String, greeting: String = "Hello") = "$greeting, $name!"

val func = ::greet
val params = func.parameters.associateBy { it.name }

// 只传 name,greeting 用默认值
val result1 = func.callBy(mapOf(params["name"]!! to "Kotlin"))
println(result1) // Hello, Kotlin!

// 传两个参数
val result2 = func.callBy(mapOf(
    params["name"]!! to "World",
    params["greeting"]!! to "Hi"
))
println(result2) // Hi, World!
我的习惯: 在写工具类或者注解处理器的时候,我更喜欢用 callBy()。因为参数名比位置更可靠,不容易出错。而且可以只传部分参数,剩下的走默认值。

3. 调用构造函数

反射也能创建对象。用 KClass.createInstance() 或者 primaryConstructor?.call()

data class Point(val x: Int, val y: Int)

// 方式一:无参构造(要求类有无参构造)
// val point1 = Point::class.createInstance() // 报错,Point 没有无参构造

// 方式二:通过主构造器
val constructor = Point::class.primaryConstructor
val point2 = constructor?.call(10, 20)
println(point2) // Point(x=10, y=20)

这里要注意,createInstance() 要求类必须有一个无参构造函数。如果你的类没有,就得用 primaryConstructor 手动传参。

避坑指南

反射虽然强大,但坑也不少。我把自己踩过的坑列出来,你遇到了可以少走弯路。

问题 原因 解决方案
调用私有方法报错 访问权限检查 设置 isAccessible = true
泛型类型擦除 JVM 泛型擦除导致 List<String> 变成 List 使用 reifiedtypeOf<T>() 保留类型
性能开销大 反射比直接调用慢 10-100 倍 缓存反射对象,避免频繁调用
混淆后找不到成员 ProGuard/R8 重命名了类和方法 添加混淆规则,保留反射调用的成员

我曾经在线上环境遇到过一个问题:用反射调用一个混淆后的方法,结果方法名被改成了 a(),直接崩溃。从那以后,我所有反射调用的类和方法都会在混淆配置里加 -keep 规则。

性能提醒: 反射不适合在循环中高频调用。如果你需要在循环里反复调用同一个方法,建议把反射得到的 KFunctionMethod 缓存起来,不要每次都重新查找。

总结

反射是 Kotlin 元编程的基石。它让你在运行时获得类型信息,动态调用函数和属性。虽然性能不如直接调用,但在框架开发、注解处理、序列化、依赖注入等场景下,反射几乎是唯一的选择。

我个人建议:能用普通调用就别用反射。但当你需要处理「编译期未知的类型」时,反射就是你最好的朋友。记住三个核心操作:::class 拿类型,::fun 拿函数,call/callBy 执行调用。把这套组合拳练熟,大部分反射需求都能搞定。


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