Kotlin 的注解与反射

声明并应用注解

一个注解允许你把额外的元数据关联到一个声明上,然后元数据就可以被相关的源代码工具访问,通过编译好的类文件或是在运行时,这取决于这个注解是如何配置的。

应用注解

kotlin 中使用注解的方法和 Java 一样,要应用一个注解,以 @ 字符作为(注解)名字的前缀,并放在要注解的声明最前面或上面。例如,可以用 @Test 标记一个测试方法:

@Test
fun addition_isCorrect() {
    assertEquals(4, 2 + 2)
}

@Test fun testTrue() {
    Assert.assertTrue(true)
}

我们再看一下 @Deprecated 注解,它在 kotlin 中的含义和 Java 一样,但是 kotlinreplaceWith 参数增强了它,让你可以提供一个替代者的匹配模式,以支持平滑地过渡到 API 的新版本。例如:

@Deprecated("Use removeAt(index) instead.", ReplaceWith("removeAt(index)"))
fun remove(index: Int) {/*..*/}

如果 remove 方法被调用了,IDE 会提示应该使用哪个函数来代替它,还会提供一个自动的快速修正。

注解只能拥有如下类型的参数: 基本数据类型、 字符串、 枚举、 类引用、 其他的注解类,以及前面这些类型的数组。指定注解实参的语法与 Java 有些微小的区别。

  • 要把一个类指定为注解实参,在类名后加上 ::class ,例如: @MyAnnotation(MyClass::class)
  • 要把另一个注解指定为一个实参,去掉注解名称前面的 @ ,例如: 前面例子中的 ReplaceWith 是一个注解,但是你把它指定为 Deprecated 注解的实参时没有用 @
  • 要把一个数组指定为一个实参,使用 arrayOf 函数,例如: @MyAnnotation(path = arrayOf("/a","/b"))

如果注解类是在 Java 中声明的,命名为 value 的形参按需自动地被转换成可变长度的形参,所以不用 arrayOf 函数就可以提供多个实参。

注意: 要把属性当作注解实参使用,需要用 const 修饰符标记它,来告知编译器这个属性是编译期常量,因为注解实参需要在编译期就是已知的。

// 要声明成常量才被用注解引用
const val TEST_TIMEOUT = 100L

@Test(timeout = TEST_TIMEOUT)
fun testMethod() {
}

注解目标

许多情况下,kotlin 源代码中的单个声明会对应成多个 Java 声明,而且它们都可以被添加注解。例如,一个 kotlin 属性就对应了一个 Java 字段,一个 getter 以及一个潜在的 setter 方法。因为是编译器自动帮我们生成的,那么我们应该怎样给这些方法添加注解呢?答案是:使用点目标声明被用来说明要注解的元素。使用点目标被放在 @ 符号和注解名称之间,并用冒号和注解名称分隔。例如:

// @Rule 注解被应用到了某个属性的 getter 方法上
@get:Rule 

下面我们来看一下使用这个注解的例子。 在 JUnit 中可以指定一个每个测试方法被执行之前都会执行的规则,要指定一个规则,在 Java 中需要声明一个用 @Rule 注解的 public 字段或者方法。但是如果你在 Kotlin 中只是用 @Rule 注解一个属性,运行时就会报错,因为注解的是属性(私有的):

@Rule
val folder = TemporaryFolder()

fun testMethod() {
    val createFile = folder.newFile("myfile.txt")
}

// 编译后的代码,注解的是属性
@Rule
private final TemporaryFolder folder = new TemporaryFolder();

// 抛出异常
org.junit.internal.runners.rules.ValidationError: The @Rule 'folder' must be public.

因此,我们应该显式地写出来,让 @Rule 注解作用到 Publicgetter 方法中。

@get:Rule    
val folder = TemporaryFolder()

//编译后的代码,注解的是 getter 而不是属性
@Rule
public final TemporaryFolder getFolder() {
   return this.folder;
}

如果你使用 Java 中声明的注解来注解一个属性,它会被默认地应用到相应的字段上。kotlin 支持的使用点目标的完整列表如下:

  • property —— Java 的注解不能应用这种使用点目标。
  • field —— 为属性生成的字段。
  • get —— 属性的 getter
  • set —— 属性的 setter
  • receiver —— 扩展函数或者扩展属性的接收者参数。
  • param —— 构造方法的参数。
  • setParam —— 属性 setter 的参数。
  • delegate —— 为委托属性存储委托实例的字段。
  • file —— 包含在文件中声明的顶层函数和属性的类。

我们之前就用到@file:JvmName("FunctionUtil") 来改变了对应类的名称,用到 file 目标的注解必须放在文件的顶层,放在包声明之前。

注意: 和 Java 不一样的是,kotlin 允许你对任意的表达式应用注解,而不仅仅是类和函数的声明及类型。

下面是一个注解局部变量的例子,抑制了未受检转换的警告:

fun test(list: List<*>) {
    @Suppress("UNCHECKED_CAST")
    val strings = list as List<String>
}

用注解控制 Java API

kotlin 提供了各种注解来控制 kotlin 编写的声明如何编译成字节码并暴露给 Java 调用者。其中一些注解代替了 Java 中对应的关键字:比如,注解 @Volatile@Strictfp 直接充当了 Java 的关键字 volatilestrictfp 的替身。其他的注解则是被用来改变 kotlin 声明对 Java 调用者的可见性:

  • @JvmName 将改变由 kotlin 生成的 Java 方法或字段的名称。
  • @JvmStatic 能被用在对象声明或者伴生对象的方法上,把它们暴露成 Java 静态方法。
  • @JvmOverloads 指导 kotlin 编译器为带默认参数值的函数生成多个重载方法。
  • @JvmField 可以应用于一个属性,把这个属性暴露成一个没有访问器的公有 Java 字段。

其实以上这些用来改变 kotlin 声明对 Java 调用者的可见性的注解我们都在之前的讲解中使用过了。

反射:在运行时对 kotlin 对象进行自省

反射:一种在运行时动态访问对象属性和方法的方式,而不需要事先确定这些属性是什么。

一般来说,当你访问一个对象的方法或者属性时,程序的源代码会引用一个具体的声明,编译器将静态地解析这个引用来确保这个声明是存在的。但有些时候,你需要编写能够使用任意类型的对象的代码,或者只能在运行时才能确定要访问的方法和属性的名称。JSON 序列化库就是这样的,它要能够把任何对象都序列化成 JSON,所以它不能引用具体的类和属性,反射就是用到这种场景的。

当在 kotlin 中使用反射时,你会和两种不同的反射 API 打交道,分别是 Java 的 和 kotlin 的,因为 kotlin 类会被编译成普通的 Java 字节码,所以完全可以用 Java 的反射 APIkotlin 类做处理。但是kotlin API 提供了很多扩展功能,使用起来更加方便。

注意: 在一些特别在意运行时库的大小的平台上,例如 Android ,为了降低大小,kotlin 反射 API 被打包成了单独的 .jar 文件。需要使用的时候,需要手动进行库的依赖。

kotlin 反射 API

kotlin 反射 API 的主要入口就是 KClass 类,它代表一个类。KClass 对应的是 java.lang.class ,可以用它列举和访问类中包含的所有声明,包括超类中的声明。MyClass:class 的写法会带给你一个 KClass 的实例。要在运行时取得一个对象的类,首先使用 javaClass 属性获取它的 Java 类,这等价于 Java 中的 java.lang.Object.getClass() 。然后访问该类的 .kotlin 扩展属性,从 Java 切换到 kotlin 的反射 API

fun testKotlinClass() {
    val person = Person("fanda", 18)
    val kClass= person.javaClass.kotlin
    println(kClass.simpleName)
    kClass.members.forEach { println(it) }
    println()
    kClass.memberProperties.forEach { println(it) }
    println()
    kClass.memberFunctions.forEach { println(it) }
}

由类的所有成员组成的列表是一个 KCallable 实例的集合,KCallable 是函数和属性的超类接口,它声明了 call 方法,允许你调用对应的函数或者对应属性的 getter

interface KCallable<out R> : KAnnotatedElement {
    fun call(vararg args: Any?): R
}

你把被引用函数的实参放在 varargs 列表中提供给它,下面的代码展示了如何通过反射使用 call 来调用一个函数:

fun foo(x:Int) = println(x)

val kFunction  = ::foo

//    kFunction.call(44)
kFunction.call()    //Callable expects 1 arguments, but 0 were provided.

由于 call 方法是传入可变参数的,如果参数个数有误,就会报异常,在这种情况下,可以用一个更具体的方法来调用这个函数。 ::foo 表达式的类型是 Function1<Int,Unit> ,它包含了形参类型和返回类型的信息。 1 表示这个函数接收一个形参,你可以通过 invoke 方法来调用函数,这个方法的参数类型和个数是确定的。

fun testFunctionN() {
    fun sum(x: Int, y: Int) = x + y

//    val function2 = ::sum
    // 显式声明类型
    val function2: Function2<Int, Int, Int> = ::sum
    println(function2.invoke(1, 2) + function2(3, 4))
}

可以通过 FunctionNinvoke 方法来调用函数,也可以直接调用。 call 方法是对所有类型都有效的通用手段,但是它不提供类型安全性。如果你明确形参类型和返回类型,那么应该优先使用这个具体类型的 invoke 方法。

你也可以在一个 kProperty 实例上调用 call 方法,它会调用该属性的 getter 方法,也可以调用 kPropertysetter 对象的 call 方法来设置属性的值。更好的方法是直接调用 setget 方法。

// 顶层属性
var counter =0

fun testKProperty() {
    val kProperty = ::counter
      // 调用 call 方法
//    kProperty.setter.call(42)
//    println(kProperty.call())

    // 调用 set 和 get 方法
    kProperty.set(33)
    println(kProperty.get())

}

注意:顶层属性表示为 kProperty0 接口的实例,它有一个无参数的 get 方法,一个成员属性由 kProperty1 的实例表示,它拥有一个单参数的 get 方法。要访问该属性的值,必须提供你需要的值所属的那个对象的实例。

val person = Person("fanda", 18)

//    val memberProperty = Person::age
// 显式声明
val memberProperty:KProperty1<Person,Int> = Person::age

println(memberProperty.call(person))
println(memberProperty.get(person))

注意:反射只能访问定义在最外层或者类中的属性,而不能访问函数的局部变量。

小结

  • kotlin 中应用注解的语法和 Java 几乎一模一样。
  • kotlin 中可以让你应用注解的目标的范围比 Java 更广,其中包括了文件和表达式。
  • 一个注解的参数可以是一个基本数据类型、 一个字符串、 一个枚举、 一个类引用、 一个其他注解类的实例,或者前面这些元素组成的数组。
  • 如果单个 kotlin 声明产生了多个字节码元素,像 @get:Rule 这样指定一个注解的使用点目标,允许你选择注解如何应用。
  • 注解类是一个拥有主构造方法且没有类主体的类,其构造方法中所有参数都被标记成 val 属性。
  • KFunctionKProperty 接口都继承了 KCallable ,它提供了一个通用的 call 方法。
  • KCallable.callBy 方法能用来调用带默认参数值的方法。