定义注解
Kotlin使用 annotation class 关键字(就像使用 enum class 定义枚举类一样),定义注解非常简单,Kottin 甚至不允许为注解定义注解体,也就是说,注解后面不能有花括号。
//定义一个简单的注解
annotation class MyClass
定义了该注解之后,就可以在程序的任何地方使用该注解。使用注解的语法非常类似于使 用 public、 final 这样的修饰符,通常可用于修饰程序中的类、方法、属性、接口等定义。通常会把注解放在所有修饰符之前。
//使用自Test 修饰类定义
@Test
class Demo1 {
//使用自Test 注解修饰属性
@Test
var name: String = ""
//使用自Test 注解修饰方法
@Test
fun info() {
}
}
如果要用注解来修饰主构造器,就像前面所介绍的,程序必须为主构造器添加 constructor 关键字。
class User @Test constructor(var name : String, var pass: String) { }
注解的属性和构造器
注解还可以带属性,由于注解没有注解体,因此注解的属性只能在注解声明部分指定。实际上,相当于在注解的主构造器中指定注解的属性。
由于注解与普通类不同 , 注解的属性值只能在使用时指定,并且一旦为注解的属性指定了属性值,以后就绝对不会改变其属性值,因此注解的属性只能定义为只读属性。
annotation class MyTag(val name: String , val age : Int)
使用 annotation class 定义的注解其实就相当于定义了一个注解接口,这个注解接口继承了kotlin.Annotation接口。
需要说明的是,注解的属性不能使用可空类型(不能在类型后添加“?”),这是因为JVM本身不允许使用 null作为注解的属性值。
一旦在注解中定义了属性之后 ,使用该属性时就应该为其指定属性值 ,如下面代码所示
class Item {
//使用带属性的注解时,需要为属性指定属性值
@MyTag(name="xx", age=6)
fun info() {
}
}
也可以在定义注解的属性时使用等号(=)为其指定初始值(默认值)(就像定义类时在主构造器中为类的属性指定初始值 一样) ,注解的初始值只能是编译时常量。如果为注解的属性指定了默认值,那么在使用该注解时可以不为这些属性指定值,而是直接使用默认值。
根据注解是否可以包含属性,可以把注解分为如下两类。
- 标记注解: 没有定义属性的注解被称为标记注解。这种注解仅利用自身的存在与否来提供信息,如前面所介绍的@Test等注解。
- 元数据注解: 包含属性的注解被称为元数据注解。因此它们可以接受更多的配置信息(以属性值的方式进行设置) 。 如前面所介绍的@MyTag等注解。
与 Java类似的是,如果注解的属性名为 value,则为 value属性指定属性值时可省略属性名。
Kotlin使用 vararg修饰需要指定多个值的属性(相当于数组类型的属性),也可以不带属性名。
如果将一个注解作为另一个注解的属性值,那么在使用注解时不需要以@作为前缀。
//定义带 value 属性的注解
annotation class MyTag(val value: String)
//该注解的 target 属性的类型是 MyTag
annotation class showTag(val message:String,val tag:MyTag)
@showTag(message = "SS",tag = MyTag("ZZZ"))
class Demo1
如果需要将一个类作为注解的属性,请使用 Kotlin 类( KClass), Kotlin 编译器会自动将 其转换为 Java类,以便 Java代码能够正常看到该注解和参数 。
// tag1 的类型是 KClass<*>,这是星号投影用法,相当于 Java 的原始类型
// tag2 的类型是 KClass<out Any>,这是使用处协变的用法
//可传入 KClass<Int>、 KClass<String〉等,只要尖括号里的类型是 Any 的子类即可
annotation class DrawTag(val tag1:KClass<*>,val tag2:KClass<out Any>)
@DrawTag(tag1 = String::class,tag2 = Int::class)
class Demo1
元注解
Kotlin 在 kotlin.annotation 包下提供了4个Meta 注解(元注解),这4个元注解都用于修饰其他的注解定义。
使用@ Retention
@Retention只能修饰注解定义,用于指定被修饰的注解可以保留多长时间 。@Retention元注解包含一个 AnnotationRetention类型的 value属性,所以使用@Retention时必须为该value 属性指定值。
value 属性的值只能是如下 3 个 。
- AnnotationRetention.SOURCE: 注解只保留在源代码中,编译器直接丢弃这种注解 。
- AnnotationRetention.BINARY: 编译器将把注解记录在 class 文件中 。当运行该字节码
文件时, JVM 不可获取注解信息。- AnnotationRetention.RUNTIME: 编译器将把注解记录在 class文件中。当运行该字节
码文件时, JVM也可获取注解信息,程序可以通过反射获取该注解信息。这是默认值。
如果要通过反射获取注解信息,就需要使用 value属性值为 AnnotationRetention.RUNTIME
的@Retention (或省略该元注解)。使用@Retention元注解可采用如下代码为value指定值。
//下面定义的 Testable 注解保留到运行时
@Retention(value = AnnotationRetention.RUNTIME)
annotation class Test
使用@Target
@Target 也只能修饰注解定义,用于指定被修饰的注解能修饰哪些程序单元。@Target 元注解包含一个类型为 AnnotationTarget 数组的 allowedTargets 属性,该属性的值只能是如下几个值组成的数组。
- Annotation Target.CLASS: 指定该策略的注解只能修饰类。
- AnnotationTarget.ANNOTATION_CLASS:指定该策略的注解只能修饰注解。
- AnnotationTarget.TYPE_PARAMETER:指定该策略的注解只能修饰泛型形参(目前暂时还不支持)。
- AnnotationTarget.PROPERTY:指定该策略的注解只能修饰属性。
- AnnotationTarget.FIELD: 指定该策略的注解只能修饰字段(包括属性的幕后字段)。
- AnnotationTarget.LOCAL_VARIABLE:指定该策略的注解只能修饰局部变量。
- AnnotationTarget.VALUE_PARAMETER:指 定该策略的注解只能修饰函数或构造器的形参。
- AnnotationTarget.CONSTRUCTOR: 指定该策略的注解只能修饰构造器。
- AnnotationTarget.FUNCTION: 指定该策略的注解只能修饰函数和方法(不包含构造器)。
- AnnotationTarget. PROPERTY_GETTER:指 定该策略的注解只能修饰属性的getter 方法。
- AnnotationTarget. PROPERTY_SETTER:指 定该策略的注解只能修饰属性的setter 方法。
- Annotation Target.TYPE: 指定该策略的注解只能修饰类型。
- AnnotationTarget.EXPRESSION: 指定该策略的注解只能修饰各种表达式。
- AnnotationTarget.FILE: 指定该策略的注解只能修饰文件。
- AnnotationTarget.TYPEALIAS:指定该策略的注解只能修饰类型别名。
与使用@Retention 类似的是,使用@Target 也可以直接在括号里指定value 值,而无须使用name=value 的形式。如下代码指定@ActionListenerFor注解只能修饰属性。
//指定@ActionListenerFor注解只能修饰属性
@Target(allowedTargets = AnnotationTarget.PROPERTY)
annotation class ActionListenerFor
使用@MustBeDocumented
使用@MustBeDocumented元注解修饰的注解将被文档工具提取到API文档中,如果定义注解类时使用了@MustBeDocumented修饰,则所有使用该元注解修饰的程序元素的API文档中将会包含该注解说明 。
下面代码定义了一个@Testable 注解,程序使用@MustBeDocumented 修饰该注解,所以@Testable 注解将被文档工具所提取。
//指定@ActionListenerFor注解只能修饰属性
@Retention(value = AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
//定义@Testable 注解将被文档工具所提取
@MustBeDocumented
annotation class Testable
上面代码指定了文档工具生成 API 文档时将提取@Testable的使用信息。下面代码定义了一个 MyTest类,该类中的 info()方法使用了@Testable修饰。
class MyTest {
//使用自Testable修饰info()方法
@Testable
private fun info() {
println("info")
}
}
使用@Repeatable标记可重复注解
Kotlin允许使用多个相同的注解来修饰同一个程序单元,这种注解称为可重复注解 。
开发可重复注解需要使用@Repeatable修饰,下面通过示例来介绍如何开发可重复注解。首先定义一个@FkTag注解。
//指定@ActionListenerFor注解只能修饰属性
@Retention(value = AnnotationRetention.SOURCE)
@Target(AnnotationTarget.CLASS)
@Repeatable
annotation class FkTag(val name:String="kotlin",val age:Int)
上面定义了@FkTag注解,该注解包含两个属性。程序还使用了@Repeatable 来修饰该注解,这意味着它是一个可重复注解,因此可直接使用多个@FkTag 注解修饰目标程序单元。
@FkTag(name = "xq",age = 24)
@FkTag(age = 4)
class MyTest {
private fun info() {
println("info")
}
}
需要说明的是,由于在Java 8 之前JVM 并不支持可重复注解,Kotlin 也没有办法突破该限制,因此可重复注解的@Retention 策略只能指定为 AnnotationRetention.SOURCE,这意味着可重复注解只能被 Kotlin 编译器读取,接下来 Kotlin 编译器会直接丢弃该注解信息。
使用注解
提取注解信息
使用注解修饰类、方法、属性等成员之后,这些注解不会自己生效,必须由开发者提供相应的工具来提取并处理注解信息。
Kotlin使用 kotlin.Annotation接口来代表程序元素前面的注解,该接口是所有注解的父接口。Kotlin 在kotlin.reflect 包下新增了KAnnotatedElement接口,该接口代表程序中可以接受注解的程序元素。该接口主要有如下几个实现类。
- KCallable: 代表可执行的程序实体,如函数和属性。
- KClass:代表 Kotlin 的类、接口等类型。
- KParameter: 代表函数和属性的参数。
在kotlin.reflect包下主要包含一些实现反射功能的工具类,该包所提供的反射API包含了读取运行时注解的能力。只有当定义注解时使用了@Retention(AnnotationRetention.RUNTIME) 修饰,该注解才会保留到程序运行时,JVM 才会在装载* .class 文件时读取保存在 class 文件中的注解。
KAnnotatedElement接口是所有程序元素(如 KClass、 KCallable、 KParameter)的父接口, 所以程序通过反射获取了某个程序单元对应的KAnnotatedElement对象(如 KClass、KCallable、 KParameter)之后,程序就可以调用该对象的如下属性和方法来访问注解信息。
- annotations: List<Annotation>: 该属性返回该程序单元上所有的注解。
- <T: Annotation> findAnnotation(): T?: 根据注解类型返回该程序单元上特定类型的注解。如果该类型的注解不存在,则该方法返回 null。
下面代码片段用于获取 Test类中修饰 info()方法的所有注解,井将这些注解打印出来。
//指定@ActionListenerFor注解只能修饰属性
@Retention(value = AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
//定义@Testable 注解将被文档工具所提取
@MustBeDocumented
annotation class Testable
class MyTest {
//使用自Testable修饰info()方法
@Testable
public fun info() {
println("info")
}
}
fun main(args: Array<String>) {
val aArray = MyTest::info.annotations
//遍历所有注解
for (an in aArray){
println(an)
}
}
如果需要获取某个注解里的元数据,则可以将注解转型成所需的注解类型,然后通过注解对象的属性来访问这些元数据。代码如下 :
//指定@ActionListenerFor注解只能修饰属性
@Retention(value = AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
//定义@Testable 注解将被文档工具所提取
@MustBeDocumented
annotation class Testable(val name:String="xq",val age:Int =24)
class MyTest {
//使用自Testable修饰info()方法
@Testable(name = "sq",age = 23)
fun info() {
println("info")
}
}
fun main(args: Array<String>) {
val aArray = MyTest::info.annotations
//遍历所有注解
for (an in aArray){
println(an)
if(an is Testable){
println(an.name)
println(an.age)
}
}
}
下面分别介绍两个使用注解的例子。第 一个例子中的@Testable 注解没有任何属性,它仅是一个标记注解,其作用是标记哪些方法需要测试。
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
//定义一个标记注解,不包含任何属性
annotation class Testable
class Test {
//使用@ Testable 注解指定该方法是需要测试的
@Testable
fun m1() {
}
fun m2() {
}
//使用@ Testable 注解指定该方法是需要测试的
@Testable
fun m3() {
}
fun m4() {
}
//使用@ Testable 注解指定该方法是需要测试的
@Testable
fun m5() {
}
fun m6() {
}
//使用@ Testable 注解指定该方法是需要测试的
@Testable
fun m7() {
}
fun m8() {
}
}
正如前面所提到的,仅仅使用注解来标记程序元素对程序是不会有任何影响的,这也是注解的一条重要原则。为了让程序中的这些注解起作用,接下来必须为这些注解提供一个注解处理工具。
下面的注解处理工具会分析目标类,如果目标类中的方法使用了@Testable 注解修饰,则通过反射来运行该测试方法。
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
//定义一个标记注解,不包含任何属性
annotation class Testable
class Test {
//使用@ Testable 注解指定该方法是需要测试的
@Testable
fun m1() {
}
fun m2() {
}
//使用@ Testable 注解指定该方法是需要测试的
@Testable
fun m3() {
throw RuntimeException ("参数出错了! ")
}
fun m4() {
}
//使用@ Testable 注解指定该方法是需要测试的
@Testable
fun m5() {
}
fun m6() {
}
//使用@ Testable 注解指定该方法是需要测试的
@Testable
fun m7() {
throw RuntimeException ("程序业务出现异常! ")
}
fun m8() {
}
}
inline fun <reified T : Any> processTestable() {
var passed =0
var failed = 0
val target = T::class.createInstance()
//遍历 T 对应的类里的所有方法
for (m in T::class.functions) {
//如果该方法使用了@ Testable 修饰
if (m.findAnnotation<Testable>() != null) {
try{
//调用 m方法
m.call(target)
//测试成功, passed 计数器加 1
passed++
}catch (ex:Exception){
println ("方法"+ m +"运行失败,异常:" + ex.cause)
//测试出现异常, failed 计数器加 1
failed++
}
}
}
//统计测试结果
println ("共运行了:"+ (passed + failed) +"个方法,其中:\n"+"失败了:"+ failed +"个,\n" +"成功了:"+ passed +"个!")
}
fun main(args: Array<String>) {
//处理 MyTest 类
//运行结果:方法fun test10.Test.m3(): kotlin.Unit运行失败,异常:java.lang.RuntimeException: 参数出错了!
//方法fun test10.Test.m7(): kotlin.Unit运行失败,异常:java.lang.RuntimeException: 程序业务出现异常!
//共运行了:4个方法,其中:
//失败了:2个,
//成功了:2个!
processTestable<Test>()
}
上面程序定义了 一个<reifiedT: Any> processTestable()函数, 该函数可接收一个泛型参数, 分析该泛型参数所代表的类,并运行该目标类中使用@Testable修饰的方法。
前面介绍的只是一个标记注解,程序通过判断该注解存在与否来决定是否运行指定方法。下面程序通过使用注解来简化事件编程。在传统的事件编程中总是需要通过addActionListener() 方法来为事件源绑定事件监听器,本示例则通过@ActionListenerFor 来为程序中的按钮绑定事件监听器。
package test10
import java.awt.event.ActionEvent
import java.awt.event.ActionListener
import javax.swing.JButton
import javax.swing.JFrame
import javax.swing.JOptionPane
import javax.swing.JPanel
import javax.swing.WindowConstants.EXIT_ON_CLOSE
import kotlin.reflect.KClass
//指定该注解只能修饰属性
@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
//定义一个属性,用于设置元数据
//该 listener 属性用于保存监听器实现类
annotation class ActionListenerFor(val listener: KClass<out ActionListener>)
class AnnotationTest {
val mainWin = JFrame("使用注解绑定事件监听器")
//使用注解为 ok 按钮绑定事件监听器
@ActionListenerFor(listener = OkListener::class)
val ok = JButton("确定")
//使用注解为 cancel 按钮绑定事件监听器
@ActionListenerFor(listener = CancelListener::class)
val cancel = JButton("取消")
fun init() {
//初始化界面的方法
val jp = JPanel()
jp.add(ok)
jp.add(cancel)
mainWin.add(jp)
processAnnotations(this)
mainWin.defaultCloseOperation = EXIT_ON_CLOSE
mainWin.pack()
mainWin.isVisible = true
}
}
//定义 ok 按钮的事件监听器实现类
class OkListener : ActionListener {
override fun actionPerformed(e: ActionEvent?) {
JOptionPane.showMessageDialog(null, "单击了确认按钮")
}
}
//定义cancel按钮的事件监听器实现类
class CancelListener : ActionListener {
override fun actionPerformed(e: ActionEvent?) {
JOptionPane.showMessageDialog(null, "单击了取消按钮")
}
}
fun main(args: Array<String>) {
AnnotationTest().init()
}
//处理注解的方法,其中 obj 是包含注解的对象
fun processAnnotations(obj: Any) {
//获取 obj 对象的类
val cl = obj::class
//获取指定 obj对象的所有成员,并遍历每个成员
for (prop in cl.memberProperties) {
//获取该成员上 ActionListenerFor 类型的注解
val a = prop.findAnnotation<ActionListenerFor>()
//获取属性 prop 的值
val fObj = prop.call(obj)
//如果 fObj 是 AbstractButton的实例,且 a 不为 null
if (a != null && fObj != null && fObj is AbstractButton) {
//获取 a 注解的 listener 属性值〈它是一个监听器类〉
val listenerClazz = a.listener
//使用反射来创建 listener 类的对象
val al = listenerClazz.createInstance()
//为 fObj 按钮添加事件监听器
fObj.addActionListener(al)
}
}
}
上面代码定义了两个 JButton 按钮,并使用@ActionListenerFor 注解为这两个按钮绑定了事件监听器。使用@ActionListenerFor 注解时传入了 listener 元数据,该元数据用于设定每个按钮的监听器实现类。
正如前面所提到的,如果仅在程序中使用注解是不会起任何作用的,必须使用注解处理工具来处理程序中的注解。 上面代码使用了 processAnnotations()函数来处理注解,该处理器分析目标对象中的所有属性,如果在属性前使用了@ActionListenerFor 修饰,则取出该注解中的listener元数据,并根据该元数据来绑定事件监听器。
Java 注解与 Kotlin 的兼容性
Java 注解与 Kotlin 完全兼容,只是在使用时略加注意即可。
指定注解的作用目标
根据前面的介绍我们知道, Kotlin程序往往比较简洁, Kotlin程序的一个程序单元有时候会变成Java的多个程序单元。比如:
- 带属性声明的主构造器会变成 Java 的成员变量定义、 getter方法、 setter方法(如果是读写属性)、构造器参数 。
- 属性会变成 Java 的成员变量定义、 getter方法、 setter方法(如果是读写属性) 。
这样就产生了一个问题: 有时候我们只想用注解修饰特定的程序单元,比如只希望用注解修饰属性对应的幕后字段,或者只希望用注解修饰属性对应的getter方法,那该怎么办呢?
此时就需要为注解指定作用目标,语法格式如下:
@目标 :注解(注解属性值)
如果在同 一个目标上要指定多个注解,则需要将多个注解放在方括号中,并用空格隔开,语法格式如下:
@目标 : [注解 1 (注解属性值)注解 2 (注解属性值), . . . ]
从上面的语法格式不难看出,为注解指定作用目标,其实就是在@符号和注解之间添加目标名和冒号。 Kotlin支持的目标包含如下几个。
- file: 指定注解对文件本身起作用 。
- property: 指定注解对整个属性起作用(这种目标的注解对 Java 不可见,因为 Java 并没有真正的属性) 。
- field: 指定注解对属性的幕后字段起作用。
- get: 指定注解对属性的 getter方法起作用。
- set: 指定注解对属性的 setter方法起作用 。
- receiver: 指定注解对扩展方法或扩展属性的接收者起作用 。
- param: 指定注解对构造器的参数起作用。
- setparam: 指定注解对 setter方法的参数起作用。
- delegate: 指定注解对委托属性存储其委托实例的字段起作用。
下面先看一个简单的例子,程序指定注解只对属性的 getter方法起作用。
annotation class MyTag
annotation class FkTag(val info: String)
class Item {
//指定注解只对 getter 方法起作用
//对 getter 方法应用了两个注解: MyTag、 FkTag
@get:[MyTag FkTag(info = "补充信息")]
var name = "kotlin"
}
fun main(args: Array<String>) {
//获取Item类对应的Java类 (Class对象)
val clazz = Item::class.java
//遍历 clazz 类所包含的全部方法
for (mtd in clazz.declaredMethods) {
println("一方法${mtd}上的注解如下一")
//遍历该方法上直接声明的所有注解
for (an in mtd.declaredAnnotations) {
println(an)
}
}
//遍历 clazz 类所包含的全部成员变量
for (f in clazz.declaredFields) {
println("一属性${f}上的注解如下一")
//遍历该成员变盘上直接声明的所有注解
for (an in f.declaredAnnotations) {
println(an)
}
}
}
上面代码指定 name属性的 getter方法应用了两个注解,其实就是在原来的注解用法前增加@get:部分,与原来不指定目标的注解区别并不大。
程序后面的 main()函数主要就是分析 Item 类中各方法、成员变量的注解 。 由于要分析成员变量上的注解,因此 main()函数使用了 Java 的反射 API (由于 Kotlin 并不支持单独 定义成员变量,因此 Kotlin 的反射 API 不支持直接操作成员变量),通过该 main()的运行可以看到程序中添加的两个注解只作用于属性的getter方法上 。
如果要指定注解作用于整个文件本身,则必须将注解放在 package 语句(如果有 package语句)之前,或者所有导包语句之前(如果没有package 语句) 。代码如下 :
//指定自 FileTag 注解作用于整个文件
@file: FileTag ("yeeku")
package org.crazyit.demo
如果要指定注解作用于扩展方法或扩展属性的接收者,则使用带 receiver:的注解修饰整个扩展方法或扩展属性即可。例如如下代码:
// 指定@MyTag 注解作用于扩展方法的接收者(String)
fun @receiver:MyTag String.fun() {}
使用Java注解
Kotlin完全兼容Java注解,因此可以直接在 Kotlin程序中使用Java注解。
需要说明的是,Java注解的成员变量 (相当于 Kotlin 注解的属性,后文统称为“属性”) 是没有顺序的,因此只能通过属性名来设置属性值 CKotlin注解的属性还可通过位置来设置属性值) 。
对比如下两个注解。 下面先定义一个 Java注解。
public @interface JavaTag {
String name();
int age();
}
下面再定义一个 Kotlin注解。
annotation class KotlinTag(val name:String,val age:Int)
上面两个注解基本相同,它们都包含了 name 和 age 两个属性 。接下来在程序中使用这两个注解就可看到它们的区别 。
//Kotlin 注解可通过位置来指定属性值
//第一个值传给 name 属性,第二个值传给 age 属性
@KotlinTag("kotlin", 4)
class Book {
//Kotlin 注解也可通过属性名来指定属性值
@KotlinTag(name = "xq", age = 24)
//Java注解只能通过属性名来指定属性值
@JavaTag(name = "xy", age = 22)
fun test() {
}
}
Kotlin 注解既支持使用位置来指定属性值(第一个值传给第一个属性,第二个值传给第二个属性,依此类推),也支持使用属性名来指定属性值(这是传统 Java注解的使用方式〉;而 Java注解只能通过属性名来指定属性值 。
如果 Java 注解中的 value 属性是数组类型,那么它会变成 Kotlin 注解的 vararg属性,因此直接为它传入多个属性值即可 。
public @interface JavaTag {
String[] value();
}
数组类型的 value 属性会变成 Kotlin 注解的 vararg属性,因此可以在 Kotlin 程序中按如下方式使用该注解 。
@JavaTag("kotlin", "java")
class Book
但如果其他名称的属性是数组类型,那么在 Kotlin 中使用该注解时必须显式使用 arrayOf() 函数来构建数组。例如如下 Java注解。
public @interface JavaTag {
String[] infos();
}
上面注解的 infos 属性是数组类型,因此在 Kotlin 中使用该注解时必须显式使用 arrayOf() 函数来构建数组。例如如下代码:
@JavaTag(infos=arrayOf("kotlin", "java"))
class Book
网友评论