Kotlin 注解详解:声明、应用与元注解
1. 注解概述
在 Kotlin 中,注解(Annotation)提供了一种将元数据附加到代码元素上的机制。这些元数据可以在编译时或运行时被读取,用于指导编译器行为、框架配置或进行静态分析。Kotlin 的注解系统与 Java 高度兼容,但提供了更简洁的语法和更强的类型安全。
详细讲解了 Kotlin 中注解的声明与应用方式。内容包括如何使用 @ 符号应用注解,注解参数的类型限制及 const 修饰符的使用,以及使用点目标控制注解作用范围。此外,还介绍了如何通过注解控制 Java API 可见性,利用注解定制 JSON 序列化行为,以及如何定义自定义注解和元注解。文章涵盖了从基础用法到高级特性如 KClass 引用和泛型参数支持的完整知识体系。

在 Kotlin 中,注解(Annotation)提供了一种将元数据附加到代码元素上的机制。这些元数据可以在编译时或运行时被读取,用于指导编译器行为、框架配置或进行静态分析。Kotlin 的注解系统与 Java 高度兼容,但提供了更简洁的语法和更强的类型安全。
在 Kotlin 中使用注解的方法与 Java 类似,以 @ 字符作为注解名称的前缀,并将其放在要注解的声明的最前面。
例如,使用 JUnit 框架可以用 @Test 标记一个测试方法:
@Test
fun testExample() {
assert(true)
}
注意: 在 Android Studio 中,若要使用 @Test 注解,通常需要将其放置在对应的 test 目录中才能成功识别为测试用例。
注解可以拥有参数,实参在括号中传递,就像常规函数的调用一样。
注解只能拥有如下类型的参数:
指定类为注解实参:在类名后加上 ::class。
@MyAnnotation(ClassName::class)
fun myFunction() {}
指定另一个注解为实参:去掉注解名称前面的 @。
@Deprecated(message = "Use newApi", replaceWith = ReplaceWith("newApi"))
fun oldApi() {}
指定数组为实参:使用 arrayOf 函数。
@RequestMapping(path = arrayOf("/foo", "/bar"))
fun handleRequest() {}
如果注解类是在 Java 中声明的,命名为 value 的形参会按需自动转换成可变长度的形参。
const 修饰符:注解实参需要在编译期就是已知的,所以不能引用任意的属性作为实参。要把属性当作注解实参使用,需要用 const 修饰符标记它。用 const 标注的属性必须初始化为基本数据类型或者 String 类型的值,且只能声明在一个文件的顶层或者一个 object 中。
const val MY_CONST_STRING = "Hello"
@MyAnnotation(MY_CONST_STRING)
fun doSomething() {}
Kotlin 的 @Deprecated 注解增强了 Java 的版本,允许提供 replaceWith 参数,让你提供一个替代者的匹配模式,以支持平滑地过渡到 API 新版本。若有人使用这个函数,IDEA 会提示用哪个函数代替它,并提供一个快速修正。
许多情况下,Kotlin 源代码中的单个声明会对应多个 Java 声明,且每个都能携带注解。例如,一个 Kotlin 属性对应一个 Java 字段、一个 getter、一个潜在的 setter 和它的参数。
使用点目标声明被用来说明要注解的元素。使用点目标被放在 @ 符号和注解名称之间,并用冒号和注解名称隔开。例如:@get:Rule。
在 JUnit 中可以指定一个每个测试方法被执行之前都会执行的规则。如标准的 TemporaryFolder 规则用来创建文件和文件夹并在测试结束后删除它们。
要指定一个规则,在 Java 中需要声明一个用 @Rule 注解的 public 字段或方法。如果在你的测试类中只是用 @Rule 注解了属性 folder,你会得到一个异常,因为 @Rule 被应用到了字段上,而字段默认是私有的。要把它应用到 (公有的) getter 上,要显式地写出来,@get:Rule。
| 使用点目标 | 含义 |
|---|---|
| property | 整个属性声明 |
| field | 为属性生成的字段 |
| get | 属性的 getter |
| set | 属性的 setter |
| receiver | 扩展函数或者扩展属性的接收者参数 |
| param | 构造方法的参数 |
| setparam | 属性 setter 的参数 |
| delegate | 为委托属性存储委托实例的字段 |
| file | 包含在文件中声明的顶层函数和属性的类 |
任何应用到 file 目标的注解都必须放在文件的顶层,放在 package 指令之前。 @JvmName 是常见的应用到文件的注解之一,它改变了对应类的名称。
和 Java 不一样的是,Kotlin 允许你对任意的表达式应用注解,而不仅仅是类和函数的声明及类型。最常见的是 @Suppress 注解,可以用它抑制被注解的表达式的上下文中特定的编译器警告。
注解可以直接充当 Java 关键字或改变 Kotlin 声明对 Java 调用者的可见性。
@Volatile 和 @Strictfp:直接充当了 Java 关键字 volatile 和 strictfp。@JvmName:改变由 Kotlin 生成的 Java 方法或字段的名称。@JvmStatic:用在对象声明或者伴生对象的方法上,把它们暴露成 Java 的静态方法。@JvmOverload:指导 Kotlin 编译器为带默认参数值的函数生成多个重载函数。@JvmField:可以应用于一个属性,把这个属性暴露成一个没有访问器的公有 Java 字段。序列化是一个过程,把对象转换成可以存储或者在网络上传输的二进制或者文本的表示法,反序列化则是把这种表示法转换回一个对象。最常见的一种用来序列化的格式就是 JSON,包括 Jackson 和 kotlinx.serialization。
一个对象的 JSON 表示法由键值对组成:具体实例的属性名称和他们值之间的键值对。
从 JSON 表示法中取回一个对象要调用 deserialize 函数。当你从 JSON 数据中创建实例的时候,必须显式地指定一个类作为类型参数,因为 JSON 没有存储对象的类型。
你可以使用注解来定制对象序列化和反序列化的方式。当把一个对象序列化成 JSON 的时候,默认情况下这个库尝试序列化所有属性,并使用属性名称作为键。注解允许你改变默认的行为。
@JsonExclude:用来标记一个属性,这个属性应该排除在序列化和反序列化之外。@JsonName:让你说明代表这个属性的 (JSON) 键值对之中的键应该是一个给定的字符串而不是属性的名称。示例:
@JsonInclude(JsonInclude.Include.NON_NULL)
data class Person(
@JsonName("full_name")
val name: String,
@JsonExclude
val age: Int // 序列化时会排除它
)
以自定义注解为例,在 class 前加上了 annotation 修饰符。
注解类只是用来定义关联到声明和表达式的元数据的结构。因此编译器禁止为一个注解类指定类实体。对拥有参数的注解来说,在类的主构造方法中声明这些参数。
对一个注解类的所有参数来说,val 关键字是强制的。
在 Java 中,有一个叫作 value 的方法,而 Kotlin 注解有一个 name 属性。Java 的 value 方法很特殊:当你应用一个注解时,你要提供 value 以外所有指定特性的显式名称。而 Kotlin 中应用注解就是常规的构造方法调用。可以使用命名实参语法让实参的名称变为显式的。如果你要把 Java 中声明的注解应用到 Kotlin 元素上,必须对除了 value 以外的所有实参使用命名实参语法,而 value 会被 Kotlin 特殊对待。
和 Java 一样,一个 Kotlin 注解类自己也可以被注解。元注解是可以应用到注解类上的注解。许多依赖注入库使用了元注解来标记其他注解,表示这些注解用来识别拥有同样类型的不同的可注入对象。
标准库中定义的元注解最常见的就是 @Target。上面的 @JsonName 和 @JsonExclude 的声明使用它为这些注解指定有效的目标。
@Target 元注解说明了注解可以被应用的元素类型。如果不使用它,所有的声明都可以应用这个注解。AnnotationTarget 枚举的值列出了可以应用注解的全部可能的目标,包括:类、文件、属性、属性访问器、所有的表达式等等。如果需要还可以声明多个目标。
要声明自己的元注解,使用 ANNOTATION_CLASS 作为目标。
注意!在 Java 代码中无法使用目标为 PROPERTY 的注解,要让这样的注解可在 Java 使用,可以给它添加第二个目标 AnnotationTarget.FIELD。
它被用来说明你声明的注解是否会存储到 .class 文件,以及在运行时是否可以通过反射来访问它。Java 默认会在.class 文件中保留注解但不会让它们在运行时被访问到。大多数注解确实需要在运行时存在,所以 Kotlin 中注解拥有 RUNTIME 期。
在 JSON 库中,这出现在 @DeserializeInterface 注解中,它允许你控制那些接口类型属性的反序列化。不能直接创建一个接口的实例,因此需要指定反序列化时哪个类作为实现被创建。
当 JKid 读到一个 Person 类实例嵌套的 company 对象时,它创建并反序列化了一个 CompanyImpl 的实例,把它存储在 company 属性中。使用 CompanyImpl::class 作为 @DeserializeInterface 注解的实参来说明这一点。通常使用类名称后面跟上 ::class 关键字来引用一个类。
KClass 是 Java 的 java.lang.Class 类型在 Kotlin 中的对应类型。它用来保存 Kotlin 类的引用。KClass 的类型参数说明了这个引用可以指向哪些 Kotlin 类,例如 CompanyImpl::class 的类型是 KClass<CompanyImpl>,它是这个注解形参类型 (<out Any>) 的子类型。
默认情况下,JKid 把非基本数据类型的属性当成嵌套的对象序列化,但你可以改变这种行为并为某些值提供你自己的序列化逻辑。@CustomSerializer 注解接受一个自定义序列化器类的引用作为实参。这个序列化器应该实现 ValueSerializer 接口。
ValueSerializer 类是泛型的而且定义了一个类型形参,所以在你引用该类型的时候需要提供一个类型实参值,因为你不知道任何关于那些应用了这个注解的属性类型的信息。
KClass<out ValueSerializer<*>> 中 out 表明接收任何实现了 ValueSerializer 的接口,不只是 ValueSerializer::class,* 表明允许 ValueSerializer 序列化任何值。
@Target 明确注解可用的位置,防止误用。@Retention 策略和 @Target 枚举值是否兼容 Java 规范。const val 作为注解参数时,确保其值为编译期常量。通过合理使用注解,开发者可以极大地提升代码的可读性、安全性和可维护性,特别是在构建框架和库时,注解是实现解耦和配置的核心手段。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online