kotlin反射

Kotlin 反射的核心入口主要是 KClassKCallableKFunctionKProperty 这几个类型。官方定义里,反射就是运行时查看程序结构;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 对应 KPropertyvar 对应 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)

这里 agevar,所以可以转成 KMutableProperty1


5. 反射调用函数:KFunction

Kotlin 里函数引用用 ::,比如 ::isOddUser::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 可能识别不到,可能导致运行时 ClassNotFoundExceptionNoSuchMethodException 等问题。


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 ClassKClass
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
相关推荐
le1616161 小时前
Android Compose——尺寸修饰符的调用顺序构成的不同尺寸约束效果
android·compose·modifier
2301_789015621 小时前
Lnux权限
linux·开发语言·c++·权限
江湖中的阿龙1 小时前
Go语言零基础入门教程(一)环境搭建与基础入门
开发语言·后端·golang
集成显卡9 小时前
Rust实战七 |基于带 colored 颜色文字控制台的批量文件删除工具
开发语言·后端·rust
折翅鵬9 小时前
Android史诗级网络优化实践总结
android·网络
比昨天多敲两行10 小时前
linux 线程概念与控制
java·开发语言·jvm
huaweichenai10 小时前
php 根据每个类型的抽签范围实现抽签功能
开发语言·php
赏金术士11 小时前
Android 项目模块化与 Feature 组件实践
android·kotlin·模块化
codeejun12 小时前
每日一Go-73、云原生成本优化 —— 资源限制 & 指标驱动扩容
开发语言·云原生·golang