反射与注解:KClass、KFunction、KProperty、自定义注解、注解处理
反射和注解,这两个词听起来有点高大上,对吧?
其实说白了,反射就是让程序在运行时「偷看」自己的结构——比如看看这个类有哪些属性、哪些方法。注解呢,就是给代码贴标签,告诉编译器或框架「这里需要特殊处理」。
我个人习惯把反射和注解放在一起讲,因为在实际项目中,它们经常成对出现。比如你写个ORM框架,用注解标记字段,再用反射去读取这些标记,然后自动生成SQL语句。嗯,这套组合拳打好了,能省下大量重复劳动。
1. 反射的核心三件套:KClass、KFunction、KProperty
Kotlin的反射和Java不太一样。Java反射用的是Class、Method、Field,而Kotlin在此基础上封装了一层,提供了KClass、KFunction、KProperty。为什么要多此一举?因为Kotlin有自己特有的东西,比如数据类、密封类、协程,Java那套反射搞不定。
KClass 是Kotlin类的运行时描述。你可以通过 MyClass::class 拿到它,或者通过对象实例的 javaClass.kotlin 获取。
// 获取KClass的两种方式
val kClass1 = String::class
val kClass2 = "hello".javaClass.kotlin
println(kClass1.simpleName) // 输出: String
println(kClass2.qualifiedName) // 输出: kotlin.String
我在项目中遇到过一个问题:用Java反射拿Kotlin数据类的属性时,死活拿不到component1()这类方法。后来换成KClass.memberProperties,一下就搞定了。你想想看,Kotlin的数据类自动生成的componentN()函数,在Java反射里根本不存在,但KClass能完美识别。
KFunction 代表一个函数。你可以通过 ::函数名 获取函数引用,然后调用 call() 执行它。
fun greet(name: String): String = "Hello, $name"
val func = ::greet
println(func.call("Kotlin")) // 输出: Hello, Kotlin
KProperty 代表一个属性。它分为 KProperty0(只读属性)和 KMutableProperty0(可变属性)。
class Person(val name: String, var age: Int)
val person = Person("Alice", 25)
val nameProp = Person::name
val ageProp = Person::age
println(nameProp.get(person)) // 输出: Alice
ageProp.setter.call(person, 30)
println(ageProp.get(person)) // 输出: 30
KClass.memberProperties 比 Java 反射的 getDeclaredFields() 更安全。KClass 会自动过滤掉编译器生成的内部字段,只返回你真正定义的属性。
2. 自定义注解:给代码贴标签
注解的定义很简单,用 annotation class 关键字。但要注意,Kotlin的注解比Java更灵活——它支持在注解中使用表达式,还支持注解的注解(元注解)。
// 定义一个简单的注解
annotation class JsonField(val name: String = "")
// 使用注解
data class User(
@JsonField("user_name") val username: String,
@JsonField("user_age") val age: Int
)
注解可以放在类、函数、属性、参数上。我个人习惯给注解加上 @Target 和 @Retention 元注解,明确告诉编译器这个注解用在哪、保留到什么时候。
@Target(AnnotationTarget.PROPERTY, AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
annotation class ExcelColumn(val header: String, val order: Int = 0)
为什么要加 @Retention(RUNTIME)?因为默认是 BINARY,运行时拿不到。我曾经踩过这个坑:写了一个注解处理器,运行时死活读不到注解信息,查了半天才发现是保留策略没设对。
3. 注解处理:反射 + 注解 = 自动化
注解本身不干活,它只是标签。真正干活的是注解处理器——通常用反射来读取注解,然后执行相应逻辑。
来看一个完整的例子:把对象序列化成JSON字符串。
// 1. 定义注解
@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class JsonField(val name: String)
// 2. 使用注解
data class Product(
@JsonField("product_id") val id: Int,
@JsonField("product_name") val name: String,
val price: Double // 没有注解的字段,用默认名
)
// 3. 注解处理器
fun toJson(obj: Any): String {
val kClass = obj::class
val properties = kClass.memberProperties
val jsonParts = mutableListOf<String>()
for (prop in properties) {
val jsonField = prop.findAnnotation<JsonField>()
val fieldName = jsonField?.name ?: prop.name
val value = prop.get(obj)
jsonParts.add("\"$fieldName\": \"$value\"")
}
return jsonParts.joinToString(", ", "{", "}")
}
// 4. 测试
val product = Product(101, "Laptop", 999.99)
println(toJson(product))
// 输出: {"product_id": "101", "product_name": "Laptop", "price": "999.99"}
你看,核心逻辑就三步:
1. 用 kClass.memberProperties 拿到所有属性
2. 用 findAnnotation<JsonField>() 检查每个属性是否有注解
3. 有注解就用注解指定的名字,没有就用属性原名
null 或引用ID。
4. 反射的性能与安全
反射虽好,但别滥用。每次反射调用都比直接调用慢一个数量级。我建议只在框架代码、工具类、序列化/反序列化场景中使用反射,业务逻辑里尽量少用。
另外,反射会破坏封装性。你可以通过反射修改 private val 字段,但这通常不是好习惯。Kotlin的 KProperty 提供了 isAccessible 属性,但默认就是 true——嗯,Kotlin团队觉得既然你都用反射了,就别拦着你了。
// 修改私有属性(不推荐,但可行)
class Secret {
private val hidden = "I'm hidden"
}
val secret = Secret()
val prop = Secret::class.memberProperties.first { it.name == "hidden" }
prop.isAccessible = true // Kotlin中默认就是true
println(prop.get(secret)) // 输出: I'm hidden
5. 知识体系总览
下面这张图总结了反射与注解的核心关系:
这张图把反射和注解分成了左右两半。左边是反射的三个核心类,右边是注解的配置项。中间箭头表示「运行时读取」——注解处理器通过反射拿到注解信息,然后执行具体逻辑。底部是常见的应用场景。
6. 实战建议
如果你刚开始接触反射和注解,我建议从一个小工具开始练手。比如写一个简单的对象转Map的工具:
fun Any.toMap(): Map<String, Any?> {
return this::class.memberProperties.associate { prop ->
prop.name to prop.get(this)
}
}
data class Book(val title: String, val author: String, val year: Int)
val book = Book("Kotlin实战", "Dmitry", 2023)
println(book.toMap())
// 输出: {title=Kotlin实战, author=Dmitry, year=2023}
这个例子只有几行代码,但涵盖了反射的核心用法。等你熟悉了,再往里面加注解支持、嵌套对象处理、类型转换……一步步来,别着急。
嗯,反射和注解的内容就这些。记住一个原则:注解定义规则,反射执行规则。两者配合,能让你的代码变得极其灵活。但也要记住,灵活是有代价的——性能和维护性。用之前先问问自己:「这个场景真的需要反射吗?」
公众号:蓝海资料掘金营,微信deep3321