1. Kotlin反射基础:KClass与Class对象的获取、Kotlin反射与Java反射的对比、反射的基本操作

反射这东西,说白了就是让程序在运行时能「偷看」自己的结构。我刚开始学Kotlin时,觉得反射挺玄乎的——代码还能看代码自己?后来做框架开发多了,才发现这玩意儿是真香。今天咱们就聊聊Kotlin反射的入门三板斧。

1.1 KClass:Kotlin反射的入口

在Java里,反射的入口是Class对象。到了Kotlin,我们有了自己的反射类型——KClass。它比Java的Class更懂Kotlin的特性,比如数据类、密封类、协程这些。

获取KClass有三种方式,我平时最常用的是第一种:

// 方式一:通过类字面值(我最常用的)
val kClass: KClass<String> = String::class

// 方式二:通过对象实例
val str = "hello"
val kClassFromObj: KClass<out String> = str::class

// 方式三:通过Java的Class对象转换
val javaClass: Class<String> = String::class.java
val kClassFromJava: KClass<String> = javaClass.kotlin
小提示:我个人习惯用::class语法,简洁明了。如果你在写Java和Kotlin混编的项目,方式三会经常用到——从Java的Class转成KClass。

拿到KClass后,你能干的事就多了。比如看看这个类叫什么、有哪些属性、构造函数长啥样:

data class User(val name: String, val age: Int)

fun inspectClass() {
    val kClass = User::class
    
    println("类名: ${kClass.simpleName}")       // User
    println("完整名: ${kClass.qualifiedName}")  // com.example.User
    println("是否是数据类: ${kClass.isData}")    // true
    println("构造函数数量: ${kClass.constructors.size}")
    
    // 遍历所有属性
    kClass.memberProperties.forEach { prop ->
        println("属性: ${prop.name}, 类型: ${prop.returnType}")
    }
}

我在项目中遇到过一个问题:用qualifiedName获取匿名内部类的类名时,返回的是null。嗯,这点要注意,匿名类没有完整的包名路径。

1.2 Kotlin反射 vs Java反射:到底差在哪?

你可能会问:Java反射用了这么多年,Kotlin为啥还要搞一套?说白了,Kotlin的反射更懂Kotlin的语法糖。我列个表格,你一看就明白:

对比维度 Java反射 Kotlin反射
入口类型 Class<T> KClass<T>
属性处理 Field,不区分可变/只读 KPropertyKMutableProperty,区分val/var
空安全 不感知可空类型 通过KType.isMarkedNullable判断
数据类 无法直接识别 KClass.isData 直接判断
密封类 无法直接识别 KClass.isSealed 直接判断
协程支持 可获取挂起函数信息

举个例子,你想判断一个属性是val还是var

class Person(val name: String, var age: Int)

fun checkMutability() {
    val kClass = Person::class
    
    kClass.memberProperties.forEach { prop ->
        when (prop) {
            is KMutableProperty<*> -> println("${prop.name} 是 var,可修改")
            else -> println("${prop.name} 是 val,只读")
        }
    }
}
// 输出:
// name 是 val,只读
// age 是 var,可修改
核心区别:Java反射把一切属性都当成Field,你得自己判断有没有setter。Kotlin反射直接告诉你这是KProperty还是KMutableProperty,省心多了。

1.3 反射的基本操作:创建对象、读写属性、调用方法

光说不练假把式。咱们直接上手,看看反射到底能干啥。

1.3.1 通过反射创建对象

data class Product(val id: Int, val name: String, val price: Double)

fun createInstance() {
    val kClass = Product::class
    
    // 方式一:使用主构造函数(推荐)
    val primaryCtor = kClass.primaryConstructor
    val product1 = primaryCtor?.call(1001, "笔记本电脑", 5999.0)
    println(product1)  // Product(id=1001, name=笔记本电脑, price=5999.0)
    
    // 方式二:使用任意构造函数
    val allCtors = kClass.constructors
    val product2 = allCtors.first().call(1002, "鼠标", 99.0)
    println(product2)
}

我记得有一次写一个通用对象工厂,需要根据配置动态创建不同类型的对象。用primaryConstructor配合call方法,几行代码就搞定了。不过要注意——参数顺序必须和构造函数定义一致,否则会抛IllegalArgumentException

1.3.2 读写属性

fun accessProperties() {
    val obj = Product(1003, "键盘", 299.0)
    val kClass = obj::class
    
    // 读取属性
    val nameProp = kClass.memberProperties.first { it.name == "name" }
    val name = nameProp.get(obj)
    println("商品名称: $name")  // 商品名称: 键盘
    
    // 修改属性(需要是KMutableProperty)
    val priceProp = kClass.memberProperties.first { it.name == "price" }
    if (priceProp is KMutableProperty<*>) {
        (priceProp as KMutableProperty<Any?>).setter.call(obj, 259.0)
        println("修改后: $obj")  // Product(id=1003, name=键盘, price=259.0)
    }
}
注意:get()setter.call()时,如果属性是private的,默认会抛异常。你需要调用prop.isAccessible = true来绕过访问限制。不过我个人建议——能不用反射访问私有属性就别用,容易破坏封装性。

1.3.3 调用方法

class Calculator {
    fun add(a: Int, b: Int): Int = a + b
    private fun secretFormula(): String = "这是秘密"
}

fun invokeMethod() {
    val obj = Calculator()
    val kClass = obj::class
    
    // 调用公开方法
    val addMethod = kClass.memberFunctions.first { it.name == "add" }
    val result = addMethod.call(obj, 3, 5)
    println("3 + 5 = $result")  // 3 + 5 = 8
    
    // 调用私有方法(需要设置可访问)
    val secretMethod = kClass.memberFunctions.first { it.name == "secretFormula" }
    secretMethod.isAccessible = true
    val secret = secretMethod.call(obj)
    println("秘密: $secret")  // 秘密: 这是秘密
}

我曾经在写一个单元测试框架时,需要调用被测试类的私有方法来做验证。用isAccessible = true确实能绕过限制,但后来我反思了一下——更好的做法是让方法包级可见或者用@VisibleForTesting注解标记。反射调用私有方法,说白了是「暴力破解」,能不用就别用。

1.4 避坑指南:反射的性能与安全

反射虽好,但别滥用。我总结了几条经验:

  • 性能开销:反射调用比直接调用慢一个数量级。如果你在循环里频繁用反射,建议缓存KClassKFunction等对象,别每次都重新获取。
  • 类型安全:反射绕过了编译期类型检查。你传错参数类型,编译时不会报错,运行时才炸。所以反射代码一定要写单元测试。
  • ProGuard/R8混淆:发布Release包时,反射调用的类、方法名可能被混淆掉。记得在混淆规则里保留它们。
我的建议:反射适合做框架、库、工具类。业务代码里尽量少用。如果你发现业务代码里大量使用反射,八成是设计上出了问题——考虑用多态、策略模式或者依赖注入来替代。

好了,这一章的内容就到这。Kotlin反射的基础操作其实不难,关键是理解KClass这个入口,以及它和Java反射的差异。下一章咱们会深入聊聊KPropertyKFunction的更多玩法,到时候见。