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 原生支持KTypeKTypeParameter
空安全 不感知 通过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.javaClass.kotlin互相转换

个人经验:如果你在写一个注解处理器(APT),建议用Java反射。因为注解处理器运行在编译期,Kotlin反射库可能还没完全初始化。我在写一个编译期代码生成器时就吃过这个亏,后来全换成Java反射才稳定。

1.5 知识体系总览

下面这张图帮你理清本章的核心脉络:

Kotlin反射基础 · 知识体系 Kotlin反射 KClass KCallable Java反射对比 memberProperties constructors isData / isSealed KFunction KProperty call / callBy 性能差异 泛型支持 空安全 入口:KClass → 操作:KCallable → 对比:Java反射

这张图把本章的三个核心模块串起来了。你从KClass入手获取类型信息,通过KCallable执行调用操作,最后对比Java反射来理解各自的适用场景。说白了,这就是Kotlin反射的「三板斧」。

最后说句实在话:反射是个双刃剑。用好了能写出非常灵活的框架代码,用不好就是满屏的运行时异常。我个人建议,在Android开发中,能用注解处理器在编译期解决的问题,就别拖到运行时用反射。但当你确实需要在运行时动态处理类型信息时,Kotlin反射绝对比Java反射顺手得多。


公众号:蓝海资料掘金营,微信deep3321