1. 概述
本文将深入讲解 Kotlin 中的注解(Annotations)机制。
我们将演示如何使用注解、如何定义自定义注解、以及如何处理注解。同时,我们还会简要讨论 Kotlin 注解与 Java 注解之间的互操作性。
最后,我们会通过一个简单的类验证示例来展示如何解析和处理注解。
2. 使用注解
在 Kotlin 中,我们通过在代码元素前加上 @
符号后接注解名称来应用注解。例如,如果我们有一个名为 Positive
的注解,可以这样使用:
@Positive val amount: Float
很多注解都需要参数。注解的参数必须是编译时常量,且类型必须是以下之一:
- Kotlin 基本类型(
Int
,Byte
,Short
,Float
,Double
,Char
,Boolean
) - 枚举
- 类引用
- 注解
- 上述类型的数组
如果注解需要参数,我们可以在括号中像调用函数一样传入参数值:
@SinceKotlin(version="1.3")
如果某个注解参数本身也是一个注解,则应省略 @
符号:
@Deprecated(message="Use rem(other) instead", replaceWith=ReplaceWith("rem(other)"))
如果参数是一个类对象,需要使用 ::class
:
@Throws(IOException::class)
如果某个参数允许传入多个值(数组),可以使用 arrayOf()
或 Kotlin 1.2 之后的简化写法:
@Throws(exceptionClasses=arrayOf(IOException::class, IllegalArgumentException::class))
或者:
@Throws(exceptionClasses=[IOException::class, IllegalArgumentException::class])
3. 定义注解
要定义一个注解,使用 annotation class
关键字。注解本质上不能包含任何代码,只能声明参数。
最简单的注解不带参数:
annotation class Positive
带参数的注解类似一个主构造函数:
annotation class Prefix(val prefix: String)
在定义自定义注解时,我们需要指定它可以应用在哪些代码元素上,以及是否保留到运行时等信息。这些元信息由所谓的 元注解(meta-annotations) 来描述。
3.1. @Target
@Target
指定注解可以应用的代码元素类型。它的参数是一个 AnnotationTarget
枚举或数组,支持以下值:
CLASS
PROPERTY
FUNCTION
CONSTRUCTOR
VALUE_PARAMETER
PROPERTY_GETTER
PROPERTY_SETTER
TYPE_PARAMETER
FILE
EXPRESSION
TYPEALIAS
- ……等
如果不显式指定,默认支持:
CLASS, PROPERTY, FIELD, LOCAL_VARIABLE, VALUE_PARAMETER, CONSTRUCTOR, FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER
3.2. @Retention
@Retention
指定注解的生命周期,参数为 AnnotationRetention
枚举,支持:
SOURCE
:仅保留在源码中,不写入.class
文件BINARY
:保留到.class
文件,但不可通过反射访问RUNTIME
:保留到运行时,可通过反射访问
Kotlin 中默认是 RUNTIME
,与 Java 不同。
3.3. @Repeatable
@Repeatable
表示该注解可以在同一个元素上多次使用,它没有参数。
3.4. @MustBeDocumented
@MustBeDocumented
表示该注解应包含在生成的文档中,也没有参数。
3.5. 注解中的嵌套声明
从 Kotlin 1.3 开始,你可以在注解中嵌套声明其他注解、枚举或伴生对象。
例如,我们可以在 Parent
注解中嵌套 Child1
和 Child2
注解,并定义一个枚举 Type
:
annotation class Parent(val type: Type) {
annotation class Child1(val prop1: String)
annotation class Child2(val prop2: Int)
enum class Type { TYPE1, TYPE2 }
}
然后在类上使用这些注解:
@Parent(Parent.Type.TYPE1)
@Parent.Child1(prop1 = "sample prop")
@Parent.Child2(prop2 = 1)
class ClassUsingNestedAnnotation {
}
获取注解时,使用 findAnnotation
方法:
val child1Annotation = ClassUsingNestedAnnotation::class.findAnnotation<Parent.Child1>()
assertEquals("sample prop", child1Annotation?.prop1)
val child2Annotation = ClassUsingNestedAnnotation::class.findAnnotation<Parent.Child2>()
assertEquals(1, child2Annotation?.prop2)
val parentAnnotation = ClassUsingNestedAnnotation::class.findAnnotation<Parent>()
assertEquals(Parent.Type.TYPE1, parentAnnotation?.type)
✅ 注意:嵌套注解需要使用 kotlin.reflect.full.findAnnotation()
方法获取。
4. 与 Java 的互操作性
Kotlin 的注解设计在很多方面与 Java 兼容,但也有细微差别。比如在 Kotlin 中声明一个属性:
val name: String?
编译器会自动生成 getter、setter 和私有字段。那么注解到底应用在哪个元素上呢?
在 Kotlin 中,如果你将一个 Java 注解加在属性上,它默认会应用在对应的字段上。
4.1. 使用点目标声明(Use-site Target Declarations)
如果你希望注解应用在 getter、setter 或其他位置,可以使用 use-site target 声明。
语法如下:
@get:Positive
@field:Positive
@set:Positive
支持的 use-site targets 包括:
field
:字段get
/set
:getter/setterparam
:构造函数参数property
:Kotlin 属性(Java 无法访问)receiver
:扩展函数的接收者delegate
:委托属性的字段file
:文件级声明
⚠️ 注意:某些 Java 注解要求字段是 public,比如 JUnit 的 @Rule
,此时必须使用 use-site target 明确指定作用位置。
4.2. JVM 相关注解
Kotlin 提供了一些注解来控制生成的 Java 字节码行为:
注解 | 说明 |
---|---|
@JvmName |
指定生成的 Java 方法或字段名 |
@JvmStatic |
标记为 static 方法或字段 |
@JvmOverloads |
生成带默认参数的重载方法 |
@JvmField |
生成 public 字段,无 getter/setter |
此外,Java 的一些关键字在 Kotlin 中以注解形式存在:
Java | Kotlin |
---|---|
@Override |
override |
volatile |
@Volatile |
strictfp |
@Strictfp |
synchronized |
@synchronized |
transient |
@Transient |
throws |
@Throws |
5. 注解处理
为了展示注解的实际用途,我们可以实现一个简单的验证器。
假设我们有一个 Item
类:
class Item(
@Positive val amount: Float,
@AllowedNames(["Alice", "Bob"]) val name: String)
我们希望验证 amount
是否为正数,name
是否在允许范围内。
处理逻辑大致如下:
val fields = item::class.java.declaredFields
for (field in fields) {
if (field.isAnnotationPresent(Positive::class.java)) {
val value = field.get(item) as Float
require(value > 0) { "amount must be positive" }
}
if (field.isAnnotationPresent(AllowedNames::class.java)) {
val allowedNames = field.getAnnotation(AllowedNames::class.java)?.names
val value = field.get(item) as String
require(value in allowedNames!!) { "name not allowed" }
}
}
⚠️ 注意:注解处理依赖 Java 的反射 API,性能开销较大,不建议在高频调用路径中使用。
6. 总结
本文讲解了 Kotlin 注解的使用方法、自定义方式以及处理机制。我们还探讨了 Kotlin 注解与 Java 的互操作性,以及一些常见使用技巧。
Kotlin 的注解机制与 Java 类似,但语法更简洁,功能更强大。如果你熟悉 Java 注解,学习 Kotlin 注解会非常轻松。
完整代码示例可在 GitHub 仓库 中查看。