反射机制简介
反射是 Java 及 Kotlin 中非常强大的特性,它允许程序在运行期间动态加载类、检查类的信息,创建对象、操作属性以及调用方法。
打个比方,我们平时写代码是在"按图纸建房子"(编译期就决定了一切);而反射则是在房子建好后,还能动态地砸墙、换家具。虽然它破坏了封装性,但带来了极大的灵活性。很多知名的开源框架(如 Spring、Gson、Retrofit)底层都重度依赖反射。
Java 反射基础与 Class 对象
反射的所有操作都基于 Java 的字节码(.class 文件)。每一个 Java 类在被 JVM 加载之后,都会在内存中生成一个唯一的 Class 对象。
Class 是一个特殊的类,用于描述运行时的类的元信息,它包含了类的名称、构造函数、方法、属性、注解、父类及实现的接口等:
- 属性对应
Field - 构造函数对应
Constructor - 方法对应
Method
下面我们通过代码来浏览反射的基本使用。
获取 Class 对象的三种方式
kotlin
class User {
var name: String = ""
private var age: Int = 0
constructor()
constructor(name: String, age: Int) {
this.name = name
this.age = age
}
private fun sayHi() {
println("hi $name")
}
}
kotlin
// 通过类静态获取,这种方式最安全,不会触发静态代码块的执行,也不会初始化静态变量
val clazz1: Class<User> = User::class.java
// 通过实例获取
val user = User("张三", 19)
val clazz2: Class<User> = user.javaClass
// 通过全类名获取,这是获取 Class 对象最动态、最强大的方式(常用于读取配置实例化对象)
val clazz3: Class<*> = Class.forName("你的包名.User")
注意:
User::class.java获取的是 Java 中的java.lang.Class<T>,而User::class是 Kotlin 专属的反射类KClass<T>。
默认情况下,Class.forName(全类名) 会同时完成类的加载和初始化(即读取字节码、生成 Class 对象,也会执行静态代码块)。它还有一个重载方法可以控制是否立即初始化。
java
public static Class<?> forName(String name, boolean initialize, ClassLoader loader)
通过构造器反射创建对象
kotlin
// 获取无参构造并创建实例
val noArg: Constructor<User> = clazz.getDeclaredConstructor()
val user1 = noArg.newInstance()
// 获取特定的有参构造并创建实例
val argCtor: Constructor<User> =
clazz.getDeclaredConstructor(String::class.java, Int::class.java)
val user2 = argCtor.newInstance("李四", 22)
getConstructor() 与 getDeclaredConstructor() 的区别:
-
getConstructor()只能获取 public 的构造器,但包含了父类的public成员。 -
getDeclaredConstructor()能获取类声明的所有构造器(包括private),但不会包含父类成员。如果需要父类的私有成员,必须通过clazz.superclass向上遍历。
反射读写成员变量
反射读写成员变量同样区分 getField(仅公有) 和 getDeclaredField(所有声明)。
kotlin
val clazz = User::class.java
val user = User("张三", 10)
// 私有字段 age,需要解除访问权限检查
val ageField: Field = clazz.getDeclaredField("age")
ageField.isAccessible = true // 必须开启,否则抛出 IllegalAccessException
ageField.set(user, 99)
println(ageField.get(user))
Kotlin 与 Java 的字段差异:在 Kotlin 中声明的 var name: String,虽然看起来是 public 的,但底层编译成 Java 字节码时,其实是一个 private String name,加上公有的 getName 和 setName 方法。
所以,如果我们直接通过 getField 获取,会抛出 NoSuchFieldException。如果想让它在编译后变为真正意义上的 public 字段,同时不生成 getter/setter 方法,需要加上 @JvmField 注解:
kotlin
@JvmField
var name: String = "" // 此时在字节码层面,就是 public String name
Kotlin 常用的其他注解:
@JvmStatic:将 Kotlin 伴生对象的方法编译成 Java 的静态方法。@JvmName:修改编译后 Java 的类 / 方法名。
反射调用函数
kotlin
val clazz = User::class.java
val user = User("王五", 30)
// 获取私有方法 sayHi,参数为空
val sayHi: Method = clazz.getDeclaredMethod("sayHi")
sayHi.isAccessible = true
// 传入执行该方法的实例对象,参数为空;如果是静态方法,传 null 即可
sayHi.invoke(user)
Class 常用属性及反射性能优化建议
kotlin
clazz.isInterface // 是否是接口
clazz.isAnnotation // 是否是注解
clazz.isEnum // 是否是枚举
clazz.isArray // 是否是数组
clazz.isPrimitive // 是否是基本类型 (如 int, boolean 等)
val superClazz = clazz.superclass // 获取父类 Class
val interfaces = clazz.interfaces // 获取实现的接口数组
println(clazz.name) // 全限定类名 (包含包名)
println(clazz.simpleName) // 简单类名 (如 User)
反射虽然很强大,但存在一定的性能损耗(跳过了编译期优化、产生额外对象、包含权限检查、以及字符串匹配查找等)。
优化建议:如果在高频执行的代码中使用反射,一定把获取到的 Method 或 Field 对象缓存到内存中(通常使用 ConcurrentHashMap)。这样能省去费时的 getDeclaredMethod 的查找,从而大幅降低性能开销。
kotlin
// 将类名和方法名拼接作为 Key 进行缓存
val methodCache = ConcurrentHashMap<String, Method>()
fun getCachedMethod(clazz: Class<*>, methodName: String): Method {
val key = "${clazz.name}#$methodName"
return methodCache.getOrPut(key) {
clazz.getDeclaredMethod(methodName).apply { isAccessible = true }
}
}
Kotlin 专属反射:KClass、元数据与默认参数调用
KClass 是 Kotlin 对 Java Class 的封装。它能读取 Kotlin 编译器附加的元数据,认识 Kotlin 独有的概念(如 val/var、data class、空安全 nullable?、扩展属性等)。
为什么 KClass 懂 Kotlin?
Kotlin 编译器在生成
.class文件时,会在类上偷偷加一个@Metadata注解。这个注解以 Protobuf 格式,记录了 Kotlin 独有的语法信息。KClass 在运行时就是通过解析这段元数据,来还原 Kotlin 代码的原貌的。
要使用这些反射特性,需要引入依赖:
kotlin
implementation("org.jetbrains.kotlin:kotlin-reflect:2.3.0")
kotlin-reflect库的体积较大(约 2.5MB),同时解析元数据的性能开销高,一般更建议使用 Java 的原生反射,或者通过 KSP/KAPT 在编译期生成代码。
Kotlin 反射实战
kotlin
data class User(
var name: String = "",
private var age: Int = 0,
val email: String?,
) {
lateinit var address: String
constructor() : this("default", 0, null)
private fun sayHi(msg: String): String = "Hi $name , $msg"
// 带有默认参数的方法
fun register(vip: Boolean = false, channel: String = "App") {
println("vip=$vip, channel=$channel")
}
}
fun main() {
val kCls: KClass<User> = User::class
val mainConstructor = kCls.constructors.first {
it.parameters.size == 3
}
val user = mainConstructor.call("张三", 23, "zhangsan@test.com").apply { address = "龙国" }
// 获取属性 (KProperty)
val nameProp = kCls.memberProperties.first { it.name == "name" }
println("name value is ${nameProp.get(user)}")
println("is var? ${nameProp is KMutableProperty<*>}") // 判断是否为 var (可变属性)
val emailProp = kCls.memberProperties.first { it.name == "email" }
println("is nullable? ${emailProp.returnType.isMarkedNullable}") // 识别可空类型
// 私有属性同样需要 isAccessible
val ageProp = kCls.declaredMemberProperties.first { it.name == "age" }
ageProp.isAccessible = true
println("private age value is ${ageProp.get(user)}")
// 判断是否延迟初始化
val addressProp = kCls.declaredMemberProperties.first { it.name == "address" }
println("address is lateinit? ${addressProp.isLateinit}")
// 类的元信息
println("is data class? ${kCls.isData}")
println("is sealed? ${kCls.isSealed}")
// 调用默认参数的方法 (KFunction)
val regFun = kCls.memberFunctions.first { it.name == "register" }
// 如果用 .call(),必须传完整参数
// 使用 .callBy(),可以通过 Map 传入指定的参数,未指定的会自动使用 Kotlin 定义的默认值
val instanceParam = regFun.parameters.first { it.kind == KParameter.Kind.INSTANCE }
val channelParam = regFun.parameters.first { it.name == "channel" }
regFun.callBy(
mapOf(
instanceParam to user, // Kotlin 反射调用成员方法时,必须传入实例对象
channelParam to "Web" // vip 没传,将自动使用默认值 false
)
)
}
注解原理、属性裂变隔离与 View 自动注入实战
注解本身没有任何逻辑,它的作用仅仅是给类、方法、字段或参数打上一个标记。
在 Kotlin 中定义注解使用的是 annotation class,Java 中使用 @interface。自定义注解时必须使用元注解(修饰注解的注解)来规范其行为。
核心元注解
-
@Target:限制注解可以放在哪些位置(如类、方法、字段等)在 Kotlin 中,它接受的是
AnnotationTarget枚举,它不仅包含了 Java 中的CLASS、FIELD、METHOD,还增加了针对 Kotlin 特性的PROPERTY(属性)、PROPERTY_GETTER、PROPERTY_SETTER等。 -
@Retention:指定注解的生命周期。SOURCE只保留到源码阶段,编译后就会丢弃。BINARY(对应 Java 的CLASS) 编译进 class 文件,运行时不可见。RUNTIME保留到运行时。只有声明为 RUNTIME,反射才能在运行时读取到该注解。
Kotlin 的属性裂变与使用处目标
前面我们也有提到,Kotlin 中的 var 在编译后就会发生裂变。变为:私有的 Field、Getter 方法、Setter 方法。
这就带来了一个问题,如果你写了 @MyAnno var name = "",那么这个注解最终会被打在谁身上?
为了消除歧义,Kotlin 中引入了使用处目标机制。通过添加前缀来明确指定注解底层作用的元素。
-
@field:MyAnno:强行打在底层的 Java 私有字段上(最常配合反射使用)。 -
@get:MyAnno:打在生成的 getter 方法上。 -
@set:MyAnno:打在生成的 setter 方法上。
实战:手写 View 自动注入
如今
findViewById的繁琐早已被 ViewBinding 或是 Jetpack Compose 解决,这里是为了演示"注解 + 反射"的高阶用法,生产环境中会因为性能问题而采用编译期注解处理(APT/KSP)。
- 定义注解:
kotlin
@Retention(AnnotationRetention.RUNTIME) // 必须是运行时保留,反射才能拿到
@Target(AnnotationTarget.FIELD) // 明确指定作用于底层的 Java FIELD
annotation class ViewById(
val viewIdName: String // 注解参数,传入ID的字符串。
)
注解参数需要是编译期常量 ,但 AGP 8+ 中
R.id默认非 final,所以我们只好传入ID的字符串。
- 创建工具类:用于在 Activity 中自动实例化被
ViewById注解标记的 View 成员。
kotlin
object ViewUtils {
@SuppressLint("DiscouragedApi")
fun inject(activity: AppCompatActivity) {
val pakName = activity.packageName
var currentClass: Class<*>? = activity.javaClass
// 向上遍历父类查找注解,需要过滤掉系统类
while (currentClass != null) {
val className = currentClass.name
// 如果遍历到了 Android 或 Java 系统层类,直接停止
if (className.startsWith("android.") || className.startsWith("java.")) {
break
}
for (field in currentClass.declaredFields) {
// 获取带有 ViewById 注解的字段
val anno = field.getAnnotation(ViewById::class.java) ?: continue
// 动态获取资源 ID
val resId = activity.resources.getIdentifier(anno.viewIdName, "id", pakName)
if (resId == 0) throw IllegalArgumentException("找不到ID: ${anno.viewIdName}")
// 实例化 View
val view = activity.findViewById<View>(resId) ?: continue
// 暴力反射注入
field.isAccessible = true
field.set(activity, view)
}
currentClass = currentClass.superclass
}
}
}
- 在 Activity 中使用。
kotlin
class MainActivity : AppCompatActivity() {
// 明确把注解打在底层的 Java Field 上
// 这样在反射时才能通过 declaredFields 找到
@field:ViewById("textView")
private lateinit var textView: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 触发注入
ViewUtils.inject(this)
// 注入后直接使用
textView.text = "Hello Reflection!"
}
}
泛型擦除的本质与 TypeToken 获取真实类型的原理
泛型,即"类型参数化",主要是为了在编译期提供类型安全,避免大量的强制类型转换。
关于泛型的基础,这里就不多说了,我们重点说说泛型擦除机制 和反射获取泛型的原理。
泛型真的被"全部擦除"了吗?
"泛型只存在于编译期,会在运行时被全部擦除"。这句话其实不太准确。
准确来说是对象实例上的泛型信息被擦除了,例如 new ArrayList<String>(),在堆内存中它只是一个普通的 ArrayList,没有任何 <String> 的标志。
但是,类、接口声明和方法签名上的泛型信息还存在,它们被编译器以字符串的形式(如 <T:Ljava/lang/Object;>),偷偷保留在了 .class 文件的 Signature(签名)属性里。反射就是通过读取这部分元数据来获取泛型的。
TypeToken 的底层原理:继承关系捕获
如果说实例化对象上的泛型会被擦除,那么在运行时怎么拿到一个对象具体的泛型类型?
答案是创造一个继承关系 ,继承时会指定泛型实参,这个实参类型会被记录到子类的字节码 Signature 中。
kotlin
open class Base<T>
// 普通子类:指定泛型实参为 String
class StringSub : Base<String>()
反射获取泛型:
kotlin
fun getSuperGenericType(clazz: Class<*>): Type? {
// 获取带泛型信息的父类Type对象 (底层就是去读取 Signature 属性)
val genericSuper = clazz.genericSuperclass
// 判断是否是带泛型的类型
if (genericSuper is ParameterizedType) {
// 获取泛型实际参数的数组
val args = genericSuper.actualTypeArguments
return args[0]
}
return null
}
如果不想每次都定义一个具体的子类,可以利用匿名内部类,这也是 Gson 中 TypeToken 的实现方式:
kotlin
fun main() {
val t1 = getSuperGenericType(StringSub::class.java)
println(t1) // 输出: class java.lang.String
// 使用匿名内部子类,捕获泛型信息
val anonymousObj = object : Base<List<Int>>() {}
val superType = anonymousObj.javaClass.genericSuperclass
if (superType is ParameterizedType) {
val arg = superType.actualTypeArguments[0]
println(arg)
// 输出: java.util.List<? extends java.lang.Integer>
}
}
为什么必须要加末尾的
{}?如果不加
{},相当于只是创建了一个普通实例,泛型依然会被擦除。加上{}代表创建了一个继承自 Base 的匿名子类。这样编译器才会将泛型实参保留在子类的 Signature 中让反射读取。
为什么 List<Int> 打印出来变为了 List<? extends Integer>?
这是 Kotlin 为了兼容 Java,保证泛型在 JVM 上的类型安全,为泛型集合自动生成的泛型通配符 。因为在 Java 看来,Kotlin 的 List 是只读的,相当于 Java 中的 List<? extends T>。