kotlin元编程(一)一文理解 Kotlin 反射

反射是指程序可以分析、修改自身数据结构的能力。反射为静态语言补充了动态能力,使得我们可以通过类名、函数名、字段名获得操作类、函数、字段的对象,进而实现对这些程序结构的访问和修改。Kotlin 支持两种反射,一种是 Java 反射,一种是 Kotlin 的反射。

Java 的反射

由于 Kotlin 与 Java 兼容,因此 Kotlin 也可以使用 Java 的反射。下面介绍一下java反射的使用。

反射调用方法

arduino 复制代码
// 获取私有方法(需要 setAccessible(true))
val privateMethod = javaClass.getDeclaredMethod("privateMethod")
privateMethod.isAccessible = true // 突破可见性限制
println(privateMethod.invoke(MyClass())) // 输出: Private

在 Java 1.2 中,支持设置 isAccessible 来访问 Java 类的私有成员。

反射获取方法的信息

kotlin 复制代码
clazz.methods
    .forEach { method ->
        // 判断是否是静态方法
        val isStatic = Modifier.isStatic(method.modifiers)

        // 获取参数列表
        val parameterTypes: Array<Class<*>?> = method.parameterTypes
        val parameterList = Arrays.stream<Class<*>?>(parameterTypes)
            .map<String?> { obj: Class<*>? -> obj!!.getName() }
            .collect(Collectors.joining(", "))

        // 获取返回值类型
        val returnType: Class<*> = method.returnType
        val returnTypeName = returnType.getName()

        println("isStatic: $isStatic parameterList: $parameterList returnTypeName: $returnTypeName")
    }

需要注意:在Java 8以前,Java编译器不会将Java方法的参数名编译到JVM字节码中,因而我们也无法使用反射访问参数的名字。比如 Retrofit 就是使用注解来设置变量名的。Java 8为编译器增加了参数-parameters以及Parameter类来获取方法参数名

获取注解信息

Java 5引入了注解,同时在反射中增加了处理注解的API。如下代码示例所示。

kotlin 复制代码
// 定义注解
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class MyAnnotation(val value: String)

class MyClass {
    @MyAnnotation("Test")
    fun annotatedMethod() {}
}

fun main() {
    val method = MyClass::class.java.getMethod("annotatedMethod")
    val annotation = method.getAnnotation(MyAnnotation::class.java)
    println(annotation?.value) // 输出: Test
}

获取泛型信息

在 Java 中,反射可以帮助我们获取到泛型的相关信息。如下所示:

typescript 复制代码
public class MyClass {
    private final Map<String, Integer> map = Map.of("a", 1, "b", 2);

    public List<Pair<String, Integer>> getList() {
        return List.of();
    }
}    

获取代码如下:

ini 复制代码
public class MyClass {
    private final Map<String, Integer> map = Map.of("a", 1, "b", 2);

    public List<Pair<String, Integer>> getList() {
        return List.of();
    }

    public void check(List<String> value) {

    }

    public static void main(String[] args) throws NoSuchFieldException, NoSuchMethodException {
        // 获取 map 字段的泛型信息
        Field field = MyTClass.class.getDeclaredField("map");
        ParameterizedType genericType = (ParameterizedType) field.getGenericType();
        Type keyType = genericType.getActualTypeArguments()[0];
        Type valueType = genericType.getActualTypeArguments()[1];
        System.out.printf("Key: %s, Value: %s%n", keyType, valueType);

        // 获取 getList() 方法返回值的泛型信息
        Method getListMethod = MyTClass.class.getMethod("getList");
        ParameterizedType returnType = (ParameterizedType) getListMethod.getGenericReturnType();
        Type listTypeArgument = returnType.getActualTypeArguments()[0];
        System.out.println(listTypeArgument);

        // 获取 check() 方法参数的泛型信息
        for (Method method : MyTClass.class.getMethods()) {
            if ("check".equals(method.getName())) {
                Parameter[] parameters = method.getParameters();
                if (parameters.length > 0) {
                    ParameterizedType paramType = (ParameterizedType) parameters[0].getParameterizedType();
                    Type paramTypeArgument = paramType.getActualTypeArguments()[0];
                    System.out.println(paramTypeArgument);
                }
            }
        }
}

我们都知道在 Java 中泛型信息会在编译阶段被删除,但是我们为什么还可以获取这个信息呢?这是因为声明侧的泛型信息在 class 文件以 signature 的形式被保存在 Constant pool 中。这就是Java反射能获取到泛型信息的原因。

但是下面这种定义泛型的方式,是无法通过上述方式来获取传入的参数泛型类型的。

php 复制代码
public <T> void function(List<T> value) {

}

那么如何解决这个问题呢?在 Gson 中,是通过定义一个 TypeToken 来实现获取传入的泛型类型。如下所示:

typescript 复制代码
// Gson 常用的情况
public List<String> parse(String jsonStr){
    List<String> topNews =  new Gson().fromJson(jsonStr, new TypeToken<List<String>>() {}.getType());
    return topNews;
}

其原理是:Class 类提供了一个方法public Type getGenericSuperclass() ,可以获取到带泛型信息的父类Type。也就是说javaclass文件会保存继承的父类或者接口的泛型信息。如下图所示:

需要注意,Class 类暂时没有提供方法来获取当前的类的泛型信息,因此只能通过父类获取。示例如下:

csharp 复制代码
System.out.println("class = " + ArrayList.class); // 输出 class = class java.util.ArrayList
System.out.println("class = " + ArrayList<String>.class); // 不支持
System.out.println("class = " + (new ArrayList<String>()).getClass()); // 输出 class = class java.util.ArrayList

kotlin 的反射

由于 Kotlin 还包含 Kotlin JS 和 Kotlin Native,因此 Kotlin 的公众标准库提供的反射能力非常有限。只有在 JVM 平台上,kotlin 才提供完整的反射功能,为此你需要添加如下依赖。

arduino 复制代码
implementation "org.jetbrains.kotlin:kotlin-reflect:1.9.10"

Kotlin JVM 反射是专门为Kotlin的语法特性设计实现的运行时元编程技术,它的出现解决了Java反射无法识别Kotlin语法特性的问题。

Kotlin 提供了很多Java不支持的特性,这些特性很难直接使用JVM字节码进行等价描述。为了确保发布的二进制产物当中包含完整的语法信息,Kotlin编译器会为每一个类生成一个 @Metadata 注解,也会为模块内所有的顶级声明(Top-Level Declaration)生成一个模块专属的元数据文件,这些文件通常以 kotlin_module 为后缀。因此可以说 Kotlin 反射本质上是读取@Metadata注解的值来还原Kotlin的原始声明信息

kotlin 反射的基本使用如下所示:

获取类的信息

kotlin 复制代码
class Person(val name: String, var age: Int)

fun main() {
    val kClass = Person::class

    // 获取类名
    println("类名: ${kClass.simpleName}") // 输出: Person

    // 获取所有属性
    val properties = kClass.members.filterIsInstance<KProperty<*>>()
    println("属性: ${properties.joinToString { it.name }}") // 输出: name, age

    // 获取所有方法
    val methods = kClass.members.filterIsInstance<KFunction<*>>()
    println("方法: ${methods.joinToString { it.name }}") // 输出: component1, component2, copy, equals, hashCode, toString 等
}

获取方法的信息

kotlin 复制代码
class Calculator {
    fun add(a: Int, b: Int): Int = a + b
}

fun main() {
    val kClass = Calculator::class

    // 获取 add 方法
    val addMethod = kClass.functions.first { it.name == "add" }

    // 输出方法信息
    println("方法名: ${addMethod.name}") // 输出: add
    println("返回类型: ${addMethod.returnType}") // 输出: kotlin.Int
    println("参数列表: ${addMethod.parameters.joinToString { "${it.name}:${it.type}" }}") // 输出: a, b

    // 调用方法
    val result = addMethod.call(Calculator(), 1, 2)
    println("调用结果: $result") // 输出: 3
}

修改属性

kotlin 复制代码
val member = Member("test", "123")
println("name = ${member.name}")
member::class.memberProperties.forEach {
    if(it.name == "name" //属性名是否对上
        && it is KMutableProperty1 //是否是可变的
        && it.setter.parameters.size == 2 //设置参数个数是否为2,第一个参数是 obj 自身,第二个是实际的值
        && it.getter.returnType.classifier == String::class //设置的类型是否为String
        ) {
        it.setter.call(member, "new test")
    }
}
println("name = ${member.name}")

获取注解信息

kotlin 复制代码
// 定义注解
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class MyAnnotation(val value: String)

// 使用注解
@MyAnnotation("类注解")
class MyAnnotationClass {
    @MyAnnotation("方法注解")
    fun myMethod() {}

    @MyAnnotation("属性注解")
    val myProperty: Int = 42
}

fun main() {
    val kClass = MyAnnotationClass::class

    // 获取类上的注解
    val classAnnotations = kClass.annotations
    println("类注解: ${classAnnotations.filterIsInstance<MyAnnotation>().first().value}")

    // 获取方法上的注解
    val method = kClass.functions.first { it.name == "myMethod" }
    val methodAnnotations = method.annotations
    println("方法注解: ${methodAnnotations.filterIsInstance<MyAnnotation>().first().value}")

    // 获取属性上的注解
    val property = kClass.memberProperties.first { it.name == "myProperty" }
    val propertyAnnotations = property.annotations
    println("属性注解: ${propertyAnnotations.filterIsInstance<MyAnnotation>().first().value}")

    // 使用 findAnnotation() 直接获取指定类型的注解:
    val myAnnotation = kClass.findAnnotation<MyAnnotation>()
    println(myAnnotation?.value) // 输出: 类注解

    // 获取 MyAnnotation 上声明的Target元注解的值
    val metaAnnotation = myAnnotation?.annotationClass?.findAnnotation<Target>()
    println(metaAnnotation?.allowedTargets?.joinToString(",") { it.name }) // 输出为: CLASS,FUNCTION,PROPERTY
}

获取泛型信息

kotlin 复制代码
// 获取 List<String> 的类型
val listType = typeOf<List<String>>()

// 输出类型信息
println("原始类型: ${listType.classifier}") // 输出: class kotlin.collections.List
println("泛型参数: ${listType.arguments}") // 输出: [kotlin.String]
kotlin 复制代码
data class Person(val name: String, val age: Int)

@OptIn(ExperimentalStdlibApi::class)
fun main() {
    val jsonStr = "[{\"name\":\"John Doe\",\"age\":30}]"
    val topNews =
        Gson().fromJson<List<Person>>(jsonStr, typeOf<List<Person>>().javaType)
    println("parse $topNews")
}

KClass、KCallable、KParameter、KType

从上面的例子可以看到,在 Kotlin 反射中,最重要的就是KClass、KCallable、KParameter、KType 四个类。它们的重要参数分别如下表所示:

KClass

KClass代表了一个 Kotlin 的类,下面是它的重要成员:

属性 作用
simpleName 类的名称,对于匿名内部类,则为 null
qualifiedName 完整的类名
members 所有成员属性和方法,类型是Collection<KCallable<*>>
constructors 类的所有构造函数,类型是Collection<KFunction>>
nestedClasses 类的所有嵌套类,类型是Collection<KClass<*>>
visibility 类的可见性,类型是KVisibility?,分别是这几种情况,PUBLIC、PROTECTED、INTERNAL、PRIVATE;
isFinal 是不是 final
isOpen 是不是 open
isAbstract 是不是抽象的
isSealed 是不是密封的
isData 是不是数据类
isInner 是不是内部类
isCompanion 是不是伴生对象
isFun 是不是函数式接口
isValue 是不是 Value Class

KCallable

KCallable 代表了 Kotlin 当中的所有可调用的元素,比如函数、属性、甚至是构造函数。下面是 KCallable 的重要成员:

属性 作用
name 属性和函数的名称;
parameters 所有的参数,类型是List,指的是调用这个元素所需的所有参数;
returnType 返回值类型,类型是 KType;
typeParameters 所有的类型参数 (比如泛型),类型是List;
call() KCallable 对应的调用方法,在前面的例子中,我们就调用过 setter、getter 的call() 方法。
visibility 可见性;
isSuspend 是不是挂起函数。

KParameter

KParameter 代表了KCallable当中的参数,它的重要成员如下:

属性 作用
index 参数的位置,下标从 0 开始;
name 参数的名称,源码当中参数的名称;
type 参数的类型,类型是 KType;
kind 参数的种类,对应三种情况:INSTANCE 是对象实例、EXTENSION_RECEIVER 是扩展接受者、VALUE 是实际的参数值。
  • KType

KType,代表了 Kotlin 当中的类型,它重要的成员如下:

属性 作用
classifier 类型对应的 Kotlin 类,即 KClass,我们前面的例子中,就是用的 classifier == String::class 来判断它是不是 String 类型的;
arguments 泛型参数;
isMarkedNullable 是否在源代码中标记为可空类型,即这个类型的后面有没有"?"修饰。

参考

相关推荐
ace望世界3 小时前
android的Parcelable
android
顾林海3 小时前
Android编译插桩之AspectJ:让代码像特工一样悄悄干活
android·面试·性能优化
叽哥3 小时前
Flutter Riverpod上手指南
android·flutter·ios
循环不息优化不止4 小时前
安卓开发设计模式全解析
android
诺诺Okami4 小时前
Android Framework-WMS-层级结构树
android
Kapaseker4 小时前
每个Kotlin开发者应该掌握的最佳实践,第二趴
kotlin
alexhilton15 小时前
面向开发者的系统设计:像建筑师一样思考
android·kotlin·android jetpack
用户237390331471 天前
Android Studio一个模板左侧文件的作用
android studio
CYRUS_STUDIO1 天前
用 Frida 控制 Android 线程:kill 命令、挂起与恢复全解析
android·linux·逆向
CYRUS_STUDIO1 天前
Frida 实战:Android JNI 数组 (jobjectArray) 操作全流程解析
android·逆向