1. Kotlin反射基础:概念、KClass与KCallable、与Java反射的对比
反射这东西,说白了就是让程序在运行时能「看穿」自己。我刚开始接触Kotlin反射时,第一反应是——这不就是Java反射那套吗?后来踩了几个坑才明白,Kotlin的反射设计思路其实更现代,也更安全。
咱们先理清一个概念:Kotlin反射不是Java反射的替代品,而是它的补充。Kotlin运行在JVM上,底层还是依赖Java反射机制,但Kotlin反射层封装了更多语言特性——比如属性、扩展函数、数据类这些Java没有的东西。
核心要点:Kotlin反射 = Java反射 + Kotlin语言特性的运行时访问能力
1.1 Kotlin反射的概念
反射,就是程序在运行时检查自身结构的能力。你想想看,正常情况下我们写代码,类型信息在编译期就确定了。但反射允许你在运行时获取类名、方法、属性,甚至调用它们。
Kotlin反射主要解决两个问题:
- 类型擦除后的信息恢复——泛型在运行时被擦除了,但反射能帮你找回一些线索
- Kotlin特有结构的访问——比如数据类的component函数、密封类的子类、协程的挂起函数等
我在项目中遇到过这样一个场景:需要根据JSON字符串动态创建不同类型的ViewModel。如果用Java反射,你得自己处理Kotlin的默认参数、空安全这些细节。用Kotlin反射,直接调用构造函数引用就行,省心不少。
注意:Kotlin反射库(kotlin-reflect.jar)大约2.5MB,如果你只在Java层用反射,没必要引入这个依赖。我见过不少项目盲目引入,APK体积白白增加。
1.2 KClass——Kotlin反射的入口
Java反射的入口是Class,Kotlin对应的是KClass。获取方式有三种:
// 方式一:通过对象实例获取
val user = User("Alice", 28)
val kClass1 = user::class
// 方式二:通过类名获取(Java方式)
val kClass2 = User::class
// 方式三:通过Java的Class转换
val javaClass = User::class.java
val kClass3 = javaClass.kotlin
我个人习惯用对象::class这种写法,可读性最好。你可能会问:::class和::class.java有什么区别?
| 表达式 | 返回类型 | 用途 |
|---|---|---|
obj::class |
KClass<T> |
Kotlin反射操作 |
obj::class.java |
Class<T> |
Java反射操作 |
User::class |
KClass<User> |
编译期已知类型 |
KClass提供了丰富的API,我挑几个最常用的:
data class User(val name: String, val age: Int)
fun inspectClass() {
val kClass = User::class
// 获取所有成员属性
val members = kClass.memberProperties
println(members.map { it.name }) // [name, age]
// 获取构造函数
val constructors = kClass.constructors
println(constructors.size) // 1
// 判断是否是数据类
println(kClass.isData) // true
// 获取所有注解
val annotations = kClass.annotations
}
小技巧:如果你只需要判断某个类是否实现了接口,用kClass.supertypes比Java的getInterfaces()更直观,因为它会返回完整的泛型信息。
1.3 KCallable——可调用元素的统一抽象
KCallable是Kotlin反射中一个很巧妙的设计。它把函数、属性、构造函数这些「可以调用」的东西统一抽象了。说白了,只要是能传参、能返回值的东西,都继承自KCallable。
它的继承体系是这样的:
KCallable<out R>
├── KFunction<out R> // 普通函数
│ ├── KFunction0 // 无参函数
│ ├── KFunction1 // 单参函数
│ └── ... // 直到KFunction22
├── KProperty<out R> // 属性
│ ├── KMutableProperty // 可变属性
│ └── KProperty0/KProperty1/KProperty2...
└── KConstructor<T> // 构造函数
为什么要有这个抽象?我举个例子你就明白了:
fun callAnything(callable: KCallable<*>, vararg args: Any?): Any? {
return callable.call(*args)
}
// 可以传函数
fun greet(name: String) = "Hello, $name"
val result1 = callAnything(::greet, "Kotlin")
// 可以传构造函数
val result2 = callAnything(::User, "Bob", 30)
// 可以传属性getter
val user = User("Alice", 28)
val nameProp = User::name
val result3 = callAnything(nameProp, user)
我曾经在写一个依赖注入框架时,就用这个特性统一处理了构造函数注入和属性注入。代码量减少了一半,而且类型安全多了。
KCallable的核心方法就两个:
call(vararg args: Any?)——直接调用,不检查参数类型callBy(args: Map<KParameter, Any?>)——按参数名调用,支持默认值
踩坑提醒:我曾经用call()调用一个带默认参数的函数,忘了传默认参数,结果抛了IllegalArgumentException。后来改用callBy()配合KParameter.isOptional判断,才搞定默认参数的问题。
1.4 与Java反射的对比
咱们用一张表直观对比一下:
| 对比维度 | Java反射 | Kotlin反射 |
|---|---|---|
| 入口类 | Class<T> |
KClass<T> |
| 函数表示 | Method |
KFunction |
| 属性表示 | Field |
KProperty |
| 构造函数 | Constructor |
KFunction(无独立类型) |
| 泛型支持 | 需要手动处理TypeVariable |
原生支持KType和KTypeParameter |
| 空安全 | 不感知 | 通过KType.isMarkedNullable判断 |
| 性能 | 较快(JVM原生) | 稍慢(多一层封装) |
| 依赖 | JDK自带 | 需要kotlin-reflect库 |
说说我的实际感受。Java反射最大的问题是——它不知道Kotlin的存在。比如你有一个Kotlin属性val name: String,Java反射看到的是private final String name加上getName()和setName()方法。而Kotlin反射直接告诉你:这是一个KProperty,有getter和setter,并且知道它是不可变的。
再举个例子,Kotlin的挂起函数:
suspend fun fetchData(): String {
delay(1000)
return "data"
}
// Java反射:看到的是 fetchData(Continuation) 方法
// Kotlin反射:通过 isSuspend 属性直接判断
val func = ::fetchData
println(func.isSuspend) // true
嗯,这里要注意:Kotlin反射的性能确实不如Java反射。我在一个性能敏感的场景(每秒反射调用上千次)做过对比,Kotlin反射大约慢30%-50%。所以我的建议是:
- 框架/库开发:优先用Kotlin反射,开发效率高
- 性能热点路径:用Java反射,或者缓存反射结果
- 混合使用:通过
KClass.java和Class.kotlin互相转换
个人经验:如果你在写一个注解处理器(APT),建议用Java反射。因为注解处理器运行在编译期,Kotlin反射库可能还没完全初始化。我在写一个编译期代码生成器时就吃过这个亏,后来全换成Java反射才稳定。
1.5 知识体系总览
下面这张图帮你理清本章的核心脉络:
这张图把本章的三个核心模块串起来了。你从KClass入手获取类型信息,通过KCallable执行调用操作,最后对比Java反射来理解各自的适用场景。说白了,这就是Kotlin反射的「三板斧」。
最后说句实在话:反射是个双刃剑。用好了能写出非常灵活的框架代码,用不好就是满屏的运行时异常。我个人建议,在Android开发中,能用注解处理器在编译期解决的问题,就别拖到运行时用反射。但当你确实需要在运行时动态处理类型信息时,Kotlin反射绝对比Java反射顺手得多。
公众号:蓝海资料掘金营,微信deep3321