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 注解中嵌套 Child1Child2 注解,并定义一个枚举 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/setter
  • param:构造函数参数
  • 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 仓库 中查看。


原始标题:Kotlin Annotations