深入理解 Java/Kotlin 反射、注解与泛型:从原理到实战

反射机制简介

反射是 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,加上公有的 getNamesetName 方法。

所以,如果我们直接通过 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)

反射虽然很强大,但存在一定的性能损耗(跳过了编译期优化、产生额外对象、包含权限检查、以及字符串匹配查找等)。

优化建议:如果在高频执行的代码中使用反射,一定把获取到的 MethodField 对象缓存到内存中(通常使用 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 中的 CLASSFIELDMETHOD,还增加了针对 Kotlin 特性的 PROPERTY(属性)、PROPERTY_GETTERPROPERTY_SETTER 等。

  • @Retention:指定注解的生命周期。

    • SOURCE 只保留到源码阶段,编译后就会丢弃。
    • BINARY(对应 Java 的 CLASS) 编译进 class 文件,运行时不可见。
    • RUNTIME 保留到运行时。只有声明为 RUNTIME,反射才能在运行时读取到该注解

Kotlin 的属性裂变与使用处目标

前面我们也有提到,Kotlin 中的 var 在编译后就会发生裂变。变为:私有的 FieldGetter 方法、Setter 方法。

这就带来了一个问题,如果你写了 @MyAnno var name = "",那么这个注解最终会被打在谁身上?

为了消除歧义,Kotlin 中引入了使用处目标机制。通过添加前缀来明确指定注解底层作用的元素。

  • @field:MyAnno:强行打在底层的 Java 私有字段上(最常配合反射使用)。

  • @get:MyAnno:打在生成的 getter 方法上。

  • @set:MyAnno:打在生成的 setter 方法上。

实战:手写 View 自动注入

如今 findViewById 的繁琐早已被 ViewBinding 或是 Jetpack Compose 解决,这里是为了演示"注解 + 反射"的高阶用法,生产环境中会因为性能问题而采用编译期注解处理(APT/KSP)。

  1. 定义注解:
kotlin 复制代码
@Retention(AnnotationRetention.RUNTIME) // 必须是运行时保留,反射才能拿到
@Target(AnnotationTarget.FIELD) // 明确指定作用于底层的 Java FIELD
annotation class ViewById(
    val viewIdName: String // 注解参数,传入ID的字符串。
)

注解参数需要是编译期常量 ,但 AGP 8+ 中 R.id 默认非 final,所以我们只好传入ID的字符串。

  1. 创建工具类:用于在 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
        }
    }
}
  1. 在 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>

相关推荐
Python私教3 小时前
多个项目怎么安全合并?先适配,再切换
后端·python·架构
hey you~3 小时前
呼叫中心系统全链路自研架构:如何实现99.999%通话高可用
架构·故障切换·多活架构·呼叫中心高可用·全链路自研·通信paas
Python私教4 小时前
从表格到管理系统:别先写页面,先补齐权限、流程和审计
数据库·后端·架构
喜欢打篮球的普通人5 小时前
LLVM Backend Lowering 从入门到实战:把 IR 变成机器码的完整链路
android·java·数据库
AI办公探索者6 小时前
多智能体协作架构:从单Agent到多Agent协同的技术演进
人工智能·ai·架构
程序员贺加贝6 小时前
轻制造SaaS的生产闭环建模-BOM工单领料报工质检与入库
java·设计模式·架构
ai_coder_ai6 小时前
实时流处理架构(Kappa 架构)在实时业务系统中的应用
架构
DS随心转APP7 小时前
AI生成的word怎么下载?AI导出鸭技术架构深度测评
人工智能·ai·架构·word·deepseek·ai导出鸭
这个DBA有点耶8 小时前
Oracle 迁移金仓兼容性评估指南:5 大维度深度拆解
数据库·oracle·架构