声明并应用注解
一个注解允许你把额外的元数据关联到一个声明上,然后元数据就可以被相关的源代码工具访问,通过编译好的类文件或是在运行时,这取决于这个注解是如何配置的。
应用注解
在 kotlin
中使用注解的方法和 Java
一样,要应用一个注解,以 @
字符作为(注解)名字的前缀,并放在要注解的声明最前面或上面。例如,可以用 @Test
标记一个测试方法:
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
@Test fun testTrue() {
Assert.assertTrue(true)
}
我们再看一下 @Deprecated
注解,它在 kotlin
中的含义和 Java
一样,但是 kotlin
用 replaceWith
参数增强了它,让你可以提供一个替代者的匹配模式,以支持平滑地过渡到 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
注解作用到 Public
的 getter
方法中。
@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
的关键字 volatile
和 strictfp
的替身。其他的注解则是被用来改变 kotlin
声明对 Java
调用者的可见性:
- @JvmName 将改变由
kotlin
生成的Java
方法或字段的名称。 - @JvmStatic 能被用在对象声明或者伴生对象的方法上,把它们暴露成
Java
静态方法。 - @JvmOverloads 指导
kotlin
编译器为带默认参数值的函数生成多个重载方法。 - @JvmField 可以应用于一个属性,把这个属性暴露成一个没有访问器的公有
Java
字段。
其实以上这些用来改变 kotlin
声明对 Java
调用者的可见性的注解我们都在之前的讲解中使用过了。
反射:在运行时对 kotlin 对象进行自省
反射:一种在运行时动态访问对象属性和方法的方式,而不需要事先确定这些属性是什么。
一般来说,当你访问一个对象的方法或者属性时,程序的源代码会引用一个具体的声明,编译器将静态地解析这个引用来确保这个声明是存在的。但有些时候,你需要编写能够使用任意类型的对象的代码,或者只能在运行时才能确定要访问的方法和属性的名称。JSON
序列化库就是这样的,它要能够把任何对象都序列化成 JSON
,所以它不能引用具体的类和属性,反射就是用到这种场景的。
当在 kotlin
中使用反射时,你会和两种不同的反射 API
打交道,分别是 Java
的 和 kotlin
的,因为 kotlin
类会被编译成普通的 Java
字节码,所以完全可以用 Java
的反射 API
对 kotlin
类做处理。但是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))
}
可以通过 FunctionN
的 invoke
方法来调用函数,也可以直接调用。 call
方法是对所有类型都有效的通用手段,但是它不提供类型安全性。如果你明确形参类型和返回类型,那么应该优先使用这个具体类型的 invoke 方法。
你也可以在一个 kProperty
实例上调用 call
方法,它会调用该属性的 getter
方法,也可以调用 kProperty
的 setter
对象的 call
方法来设置属性的值。更好的方法是直接调用 set
和 get
方法。
// 顶层属性
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
属性。 KFunction
和KProperty
接口都继承了KCallable
,它提供了一个通用的call
方法。KCallable.callBy
方法能用来调用带默认参数值的方法。