Kotlin 反射的核心入口主要是 KClass、KCallable、KFunction、KProperty 这几个类型。官方定义里,反射就是运行时查看程序结构;Kotlin 还把函数、属性、构造器都当成一等对象处理,所以比 Java 反射更贴近 Kotlin 语法本身。
1. 先加依赖
在 JVM / Android 项目里,完整 Kotlin 反射能力不默认塞进标准库,是单独的 kotlin-reflect,官方这样做是为了减少不用反射的应用运行时依赖体积。
dependencies {
implementation(kotlin("reflect"))
}
或者指定版本:
dependencies {
implementation("org.jetbrains.kotlin:kotlin-reflect:2.4.0")
}
常用 import:
import kotlin.reflect.*
import kotlin.reflect.full.*
import kotlin.reflect.jvm.*
2. KClass:类反射入口
Java 里是:
User::class.java
Kotlin 里是:
User::class
User::class 拿到的是 KClass<User>,User::class.java 拿到的是 Java 的 Class<User>。官方文档也强调,在 JVM 上 Kotlin class reference 和 Java class reference 不是同一个东西,需要通过 .java 互转。
示例类:
data class User(
val name: String,
var age: Int = 18
) {
fun say(prefix: String): String {
return "$prefix, my name is $name, age=$age"
}
private fun secret(): String {
return "secret info"
}
}
基本使用:
val kClass = User::class
println(kClass.simpleName) // User
println(kClass.qualifiedName) // com.xxx.User
println(kClass.isData) // true
println(kClass.constructors) // 所有构造器
println(kClass.members) // 属性 + 函数
对象::class 可以拿到运行时真实类型:
val obj: Any = User("Tom")
println(obj::class.qualifiedName)
3. 反射创建对象:构造器调用
3.1 普通构造器调用
val ctor = User::class.primaryConstructor!!
val user = ctor.call("Tom", 20)
println(user)
输出类似:
User(name=Tom, age=20)
这里 primaryConstructor 来自 kotlin.reflect.full。
3.2 使用默认参数:callBy
Kotlin 比 Java 反射强的一点是它能处理默认参数。
val ctor = User::class.primaryConstructor!!
val nameParam = ctor.parameters.first { it.name == "name" }
val user = ctor.callBy(
mapOf(
nameParam to "Tom"
)
)
println(user)
因为 age 有默认值 18,所以可以不传。输出:
User(name=Tom, age=18)
如果你用 call(),参数必须全部传;如果用 callBy(),有默认值的参数可以省略。
4. 反射读取属性:KProperty
Kotlin 里 val/var 是属性,不只是 Java 里的字段。官方文档里也说明,属性可以通过 :: 作为 first-class object 使用,val 对应 KProperty,var 对应 KMutableProperty。
4.1 读取所有属性
val user = User("Tom", 20)
val properties = User::class.memberProperties
for (prop in properties) {
println("${prop.name} = ${prop.get(user)}")
}
输出:
age = 20
name = Tom
注意:
memberProperties
会包含继承来的属性。
如果只想要当前类声明的属性:
User::class.declaredMemberProperties
4.2 读取指定属性
val user = User("Tom", 20)
val nameProp = User::class.memberProperties
.first { it.name == "name" }
println(nameProp.get(user))
输出:
Tom
4.3 修改 var 属性
只有 var 才能被改,val 不行。
val user = User("Tom", 20)
@Suppress("UNCHECKED_CAST")
val ageProp = User::class.memberProperties
.first { it.name == "age" } as KMutableProperty1<User, Int>
ageProp.set(user, 30)
println(user)
输出:
User(name=Tom, age=30)
这里 age 是 var,所以可以转成 KMutableProperty1。
5. 反射调用函数:KFunction
Kotlin 里函数引用用 ::,比如 ::isOdd、User::say。官方文档里也说明,函数、属性、构造器都属于 callable reference,公共父类型是 KCallable。
5.1 调用成员函数
val user = User("Tom", 20)
val function = User::class.memberFunctions
.first { it.name == "say" }
val result = function.call(user, "Hello")
println(result)
输出:
Hello, my name is Tom, age=20
重点是:
function.call(user, "Hello")
第一个参数是接收者对象,也就是 this。因为 say() 是成员函数,不是普通顶层函数。
5.2 调用顶层函数
假设有:
fun add(a: Int, b: Int): Int {
return a + b
}
可以这样:
val f = ::add
println(f.call(1, 2))
输出:
3
这种写法不需要传接收者对象,因为它不是成员函数。
6. 访问私有属性 / 私有函数
默认情况下,私有成员不能直接反射访问。
val user = User("Tom", 20)
val secretFunc = User::class.declaredMemberFunctions
.first { it.name == "secret" }
secretFunc.isAccessible = true
val result = secretFunc.call(user)
println(result)
需要这个 import:
import kotlin.reflect.jvm.isAccessible
不过 Android 里不建议大量这么做。私有 API 本来就不是稳定契约,R8 混淆优化后还可能被重命名或删除。Android 官方 R8 文档明确说,反射访问的类、字段、方法,R8 可能识别不到,可能导致运行时 ClassNotFoundException、NoSuchMethodException 等问题。
7. 读取注解
定义注解:
@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class Column(val name: String)
使用:
data class Student(
@Column("student_name")
val name: String,
@Column("student_age")
val age: Int
)
读取属性注解:
val props = Student::class.memberProperties
for (prop in props) {
val column = prop.findAnnotation<Column>()
println("${prop.name} -> ${column?.name}")
}
输出:
age -> student_age
name -> student_name
如果你要给 Java 反射读取字段注解,通常要写:
data class Student(
@field:Column("student_name")
val name: String
)
但是这时 Column 的 @Target 要支持 FIELD:
@Target(
AnnotationTarget.PROPERTY,
AnnotationTarget.FIELD
)
@Retention(AnnotationRetention.RUNTIME)
annotation class Column(val name: String)
简单理解:
@Column
val name: String
偏 Kotlin 属性视角。
@field:Column
val name: String
偏 JVM 字段视角。
这块在写 JSON、ORM、路由注解、依赖注入框架时很常见。
8. Kotlin 反射和 Java 反射互转
Kotlin 反射能转 Java 反射。
val kClass = User::class
val javaClass = kClass.java
Java 反射也能转 Kotlin 反射:
val kClass = User::class.java.kotlin
属性转 Java getter / field:
val nameProp = User::name
println(nameProp.javaGetter)
println(nameProp.javaField)
官方也给了类似例子:A::p.javaGetter 可以拿到 Java getter,A::p.javaField 可以拿到 backing field。
9. 一个实战例子:对象转 Map
这个是 Kotlin 反射比较典型的用途。
fun Any.toMap(): Map<String, Any?> {
return this::class.memberProperties.associate { prop ->
prop.name to prop.getter.call(this)
}
}
使用:
val user = User("Tom", 20)
val map = user.toMap()
println(map)
输出:
{name=Tom, age=20}
如果加上注解,可以做字段名映射:
@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class JsonName(val value: String)
data class Book(
@JsonName("book_name")
val name: String,
@JsonName("book_price")
val price: Double
)
fun Any.toJsonMap(): Map<String, Any?> {
return this::class.memberProperties.associate { prop ->
val jsonName = prop.findAnnotation<JsonName>()?.value ?: prop.name
jsonName to prop.getter.call(this)
}
}
使用:
val book = Book("Kotlin", 99.0)
println(book.toJsonMap())
输出:
{book_name=Kotlin, book_price=99.0}
这就是很多序列化框架、ORM 框架、路由框架的基础思想,只是成熟框架一般会尽量避免运行时反射,改成 KSP / kapt / 编译期生成代码。
10. 常用 API 总结
| API | 作用 |
|---|---|
::class |
获取 KClass |
.java |
KClass 转 Java Class |
.kotlin |
Java Class 转 KClass |
primaryConstructor |
主构造器 |
constructors |
所有构造器 |
memberProperties |
所有成员属性,包含继承 |
declaredMemberProperties |
当前类声明的属性 |
memberFunctions |
所有成员函数,包含继承 |
declaredMemberFunctions |
当前类声明的函数 |
call() |
调用函数 / 构造器 |
callBy() |
按参数调用,支持默认参数 |
findAnnotation<T>() |
查找指定注解 |
isAccessible = true |
访问私有成员 |
11. Android 里使用反射的注意点
Android 项目里我建议这样用:
第一,业务高频路径少用反射。比如列表绑定、启动流程、冷启动路径、频繁 JSON 解析,不建议大量运行时反射。
第二,需要反射时尽量缓存结果。比如:
object ReflectCache {
private val propertyCache = mutableMapOf<KClass<*>, Collection<KProperty1<out Any, *>>>()
fun getProperties(kClass: KClass<*>): Collection<KProperty1<out Any, *>> {
return propertyCache.getOrPut(kClass) {
kClass.memberProperties
}
}
}
第三,开启混淆后要加 keep rule。Android 官方文档说,R8 对直接调用能识别和保留,但对反射访问的类、字段、方法可能识别不到,因此需要 keep rules。
例如你通过反射调用:
Class.forName("com.example.MyPlugin")
那就要保留:
-keep class com.example.MyPlugin { *; }
如果只是保留某个方法,尽量精确:
-keepclassmembers class com.example.MyPlugin {
public void execute();
}
Android 官方也建议 keep rules 尽量窄,不要一上来就全量 -keep class xxx { *; },否则会影响优化效果。
12. Kotlin 反射 vs Java 反射
| 对比 | Kotlin 反射 | Java 反射 |
|---|---|---|
| 类入口 | KClass |
Class |
| 属性 | KProperty |
Field / Method |
| 函数 | KFunction |
Method |
| 构造器 | KFunction |
Constructor |
| 默认参数 | 支持 callBy |
不理解 Kotlin 默认参数 |
| 可空类型 | 能通过 KType 感知 |
Java 层不直接理解 |
| Kotlin 属性注解 | 支持较好 | 需要注意 @field: / @get: |
| Android 体积 | 需要 kotlin-reflect |
JDK 自带能力 |
| 性能 | 一般比直接调用慢 | 也慢,但更贴近 JVM |