前序
注解是什么?简单说注解就是一种标注(标记、标识),没有具体的功能逻辑代码。通过注解开发人员可以在不改变原有代码和逻辑的情况下在源代码中嵌入补充信息。Kotlin注解的使用和Java完全一样,声明注解类的语法略有不同。Java 注解与 Kotlin 100% 兼容。
注解的定义
注解可以把额外的元数据关联到一个声明上,然后元数据可以被反射机制或相关的源代码工具访问。
声明Kotlin的注解
Kotlin的声明注解的语法和常规类的声明非常相似,但需要在class
关键字之前加上annotation
修饰符。但Kotlin编译器禁止为注解类指定类主体,因为注解类只是用来定义关联到 声明 和 表达式 的元数据的结构。
#daqiKotlin.kt
annotation class daqiAnnotation
Java注解声明:
#daqiJava.java
public @interface daqiAnnotation {
}
注解的构造函数
注解可以有接受参数的构造函数。
其中注解的构造函数允许的参数类型有:
- 对应于 Java 原生类型的类型(Int、 Long等)
- 字符串
- 类(Foo::class)
- 枚举
- 其他注解
- 上面已列类型的数组。
注解作为注解构造函数的参数
当注解作为另一个注解的参数,则其名称不用以 @
字符为前缀:
annotation class daqiAnnotation(val str: String)
annotation class daqiAnnotation2(
val message: String,
val annotation: daqiAnnotation = daqiAnnotation(""))
类作为注解构造函数的参数
当需要将一个类指定为注解的参数,请使用 Kotlin 类 (KClass)。Kotlin 编译器会自动将其转换为 Java 类,以便 Java 代码能够正常看到该注解及参数 。
annotation class daqiAnnotation(val arg1: KClass<*>, val arg2: KClass<out Any>)
@daqiAnnotation(String::class, Int::class) class MyClass
将其反编译后,可以看到转换为相应的Java类:
@daqiAnnotation(
arg1 = String.class,
arg2 = int.class
)
public final class MyClass {
}
注意:注解参数不能有可空类型,因为 JVM 不支持将 null 作为注解属性的值存储。
Kotlin的元注解
和Java一样,Kotlin的注解类也使用元注解进行注解。用于其他注解的注解称为元注解,可以理解为最基本的标注。
Kotlin标准库中定义了4个元注解,分别是:MustBeDocumented、Repeatable、Retention、Target。
@Target
@Target用于指定可以应用该注解的元素类型(类、函数、属性、表达式等)。
查看Target的源码:
#Annotation.kt
@Target(AnnotationTarget.ANNOTATION_CLASS)
@MustBeDocumented
public annotation class Target(vararg val allowedTargets: AnnotationTarget)
@Target注解中可以同时接收一个或多个AnnotationTarget
枚举值:
public enum class AnnotationTarget {
//作用于类(包括枚举类)、接口、object对象和注解类
CLASS,
//仅作用于注解类
ANNOTATION_CLASS,
//作用于泛型类型参数(暂时不支持)(JDK8)
TYPE_PARAMETER,
//作用于属性
PROPERTY,
//作用于字段(包括枚举常量和支持字段)。
FIELD,
//作用于局部变量
LOCAL_VARIABLE,
//作用于函数或构造函数的参数
VALUE_PARAMETER,
//作用于构造函数(包括主构造函数和次构造函数)
CONSTRUCTOR,
//作用于方法(不包括构造函数)
FUNCTION,
//仅作用于属性的getter函数
PROPERTY_GETTER,
//仅作用于属性的setter函数
PROPERTY_SETTER,
//作用于类型(如方法内参数的类型)
TYPE,
//作用于表达式
EXPRESSION,
//作用于文件,可配合 file点目标 使用: (例如: @file:JvmName("daqiKotlin"))
FILE,
//作用于类型别名
@SinceKotlin("1.1")
TYPEALIAS
}
注意:Java代码中无法使用Target
为AnnotationTarget.PROPERTY
的注解。如果想让这样的注解在Java中使用,可以添加多一条AnnotationTarget.FIELD
的注解。
@Retention
@Retention 声明注解的保留策略。
查看Retention的源码:
@Target(AnnotationTarget.ANNOTATION_CLASS)
public annotation class Retention(val value: AnnotationRetention = AnnotationRetention.RUNTIME)
@Retention注解中可以接收一个AnnotationRetention
枚举值:
public enum class AnnotationRetention {
//表示注解仅保留在源代码中,编译器将丢弃该注解。
SOURCE,
//注解将由编译器记录在class文件中 但在运行时不需要由JVM保留。
BINARY,
//注解将由编译器记录在class文件中,并在运行时由JVM保留,因此可以反射性地读取它们。(默认行为)
RUNTIME
}
注意:Java的元注解默认会在.class文件中保留注解,但不会让它们在运行时被访问到。大多数注解需要在运行时存在,以至于Kotlin将RUNTIME
作为@Retention
注解的默认值。
@Repeatable
允许在单个元素上多次使用相同的该注解;
查看Repeatable的源码:
@Target(AnnotationTarget.ANNOTATION_CLASS)
public annotation class Repeatable
注意:在"尝试使用"@Repeatable
时发现,该注解必须要在Retention
元注解指定为AnnotationRetention.SOURCE
时才能重复使用,但Java的@Repeatable
元注解并没有该限制。(具体的Java @Repeatable
元注解的使用示例可以看这篇文章)。因为@Repeatable
是Java 8引入的新的元注解,而兼容Java 6的Kotlin对此有点不兼容?
@MustBeDocumented
指定该注解是公有 API 的一部分,并且应该包含在生成的 API 文档中显示的类或方法的签名中。
查看MustBeDocumented的源码:
@Target(AnnotationTarget.ANNOTATION_CLASS)
public annotation class MustBeDocumented
消失的@Inherited元注解
相对Java的5个元注解,Kotlin只提供了与其对应的4个元注解,Kotlin暂时不需要支持@Inherited
元注解。
@Inherited
注解表明注解类型可以从超类继承。具体意思是:存在一个带@Inherited
元注解的注解类型,当用户在某个类中查询该注解类型并且没有此类型的注解时,将尝试从该类的超类以获取注解类型。重复此过程,直到找到此类型的注解,或者到达类层次结构(对象)的顶部为止。如果没有超类具有此类型的注解,则查询将指示相关类没有此类注解。此注解仅适用于类声明。
Kotlin预定义的注解
Kotlin为了与Java具有良好的互通性,定义了一系列注解用于携带一些额外信息,以便编译器做兼容转换。
@JvmDefault
将Kotlin接口的默认方法生成Java 8的默认方法的字节码
查看源码:
@SinceKotlin("1.2")
@RequireKotlin("1.2.40", versionKind = RequireKotlinVersionKind.COMPILER_VERSION)
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY)
annotation class JvmDefault
前面接口和类中提到,当在Kotlin中声明一个带默认方法的接口时,往往会将这些“默认方法”声明为抽象方法,同时也会在该接口中生成一个DefaultImpls
静态内部类,并在其中定义同名的静态方法来提供默认实现。
但这样会存在一个问题,当对旧的Kotlin接口添加新的默认方法时,实现该接口的Java类需要重新实现新添的接口方法,否则会编译不通过。同是默认方法,但与Java 8引入默认方法的初衷相违背。为此,Kotlin提供了@JvmDefault
注解。对标有@JvmDefault
注解的默认方法,编译器会将其编译为Java 8的默认接口。
#daqiKotlin.kt
public interface daqiInterface{
@JvmDefault//刚添加会报错
fun daqiFunc() = println("带@JvmDefault的默认方法")
fun daqiFunc2() = println("默认方法")
}
#java文件
public interface daqiInterface {
@JvmDefault
default void daqiFunc() {
String var1 = "带@JvmDefault的默认方法";
System.out.println(var1);
}
void daqiFunc2();
public static final class DefaultImpls {
public static void daqiFunc2(daqiInterface $this) {
String var1 = "默认方法";
System.out.println(var1);
}
}
}
当你直接添加 @JvmDefault
时,编译器会报错。这时你需要在Gradle中配置以下参数:(具体Kotlin使用Gradle看官网)
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = ['-Xjvm-default = compatibility']
//freeCompilerArgs = ['-Xjvm-default = enable']
}
}
通过@JvmDefault
的注解得知,配置时可以选择-Xjvm-default = enable
或-Xjvm-default = compatibility
。这两个的区别是:
-
-Xjvm-default = enable
会从DefaultImpls
静态内部类中删除对应的方法。 -
-Xjvm-default = compatibility
仍会在DefaultImpls
静态内部类中保留对应的方法,提高兼容性。
注意:只有JVM目标字节码版本1.8(-jvm-target 1.8
)或更高版本才能生成默认方法。
@JvmField
指示Kotlin编译器不为此属性生成getter / setter并将其修饰为public。
查看源码:
@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.BINARY)
@MustBeDocumented
public actual annotation class JvmField
Kotlin声明的属性都默认使用private
修饰,并提供setter
/ getter
访问器对其进行访问。而@JvmField
就是告诉编译器不要为该属性自动创建setter
/ getter
访问器,并将对其使用public
修饰。(用在伴生对象的属性上,可生成public
修饰的static
属性)
#daqiKotlin.kt
class Person{
@JvmField
val name:String = ""
}
反编译后查看源码中只声明了一个public对象:
#java文件
public final class Person {
@JvmField
@NotNull
public final String name = "";
}
注意该注解只能用在有幕后字段的属性上,对于没有幕后字段的属性(例如:扩展属性、委托属性等)不能使用。因为只有拥有幕后字段的属性转换成Java代码时,才有对应的Java变量。
Kotlin属性拥有幕后字段需要满足以下条件之一:
- 使用默认 getter / setter 的属性,一定有幕后字段。对于 var 属性来说,只要 getter / setter 中有一个使用默认实现,就会生成幕后字段。
- 在自定义 getter / setter 中使用了 field 的属性。
@JvmName
指定生成Java类的类名或方法名。
查看源码:
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER, AnnotationTarget.FILE)
@Retention(AnnotationRetention.BINARY)
@MustBeDocumented
public actual annotation class JvmName(actual val name: String)
根据注解的声明,属性的访问器getter / setter也可以使用该注解,但属性不能使用~。
在daqiKotlin.kt文件中声明的所有函数和属性(包括扩展函数)都被编译为名为在DaqiKotlinKt的Java类的静态方法。其中文件名首字母会被改为大写,后置Kt。当需要修改该Kotlin文件生成的Java类名称时,可以使用@JvmName
名指定生成特定的文件名:
@file:JvmName("daqiKotlin")
package com.daqi.test
@JvmName("daqiStateFunc")
public fun daqiFunc(){
}
反编译可以看到生成的Java类名称已经修改为daqiKotlin
,而非DaqiKotlinKt
,同时顶层函数daqiFunc
的方法名被修改为daqiStateFunc
:
public final class DaqiKotlinKt {
@JvmName(name = "daqiStateFunc")
public static final void daqiStateFunc() {
}
}
@JvmMultifileClass
指示Kotlin编译器生成一个多文件的类。该文件具有在此文件中声明的顶级函数和属性。
查看源码:
@Target(AnnotationTarget.FILE)
@Retention(AnnotationRetention.SOURCE)
@MustBeDocumented
public actual annotation class JvmMultifileClass
当需要将多个Kotlin文件中的方法和属性归到一个Java类时,可以在多个文件中声明一样的@JvmName
,并在其下面添加@JvmMultifileClass
注解。(多个文件中声明一样的@JvmName
,但不添加@JvmMultifileClass
注解会编译不通过)
#daqiKotlin.kt
@file:JvmName("daqiKotlin")
@file:JvmMultifileClass
package com.daqi.test
fun daqi(){
}
#daqiKotlin2.kt
@file:JvmName("daqiKotlin")
@file:JvmMultifileClass
package com.daqi.test
fun daqi2(){
}
Kotlin编译器会将该两个文件中的方法和属性合并到@JvmName
注解生成的指定名称的Java类中:
@JvmOverloads
指示Kotlin编译器为此函数生成替换默认参数值的重载函数(从最后一个开始省略每个参数)。
查看源码:
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CONSTRUCTOR)
@Retention(AnnotationRetention.BINARY)
@MustBeDocumented
public actual annotation class JvmOverloads
Java并没有参数默认值的概念,当你从Java中调用Kotlin的默认参数函数时,必须显示地指定所有参数值。使用@JvmOverloads注解该方法,Kotlin编译器会生成相应的Java重载函数,从最后一个参数开始省略每个函数。
#daqiKotlin.kt
@JvmOverloads
fun daqi(name :String = "daqi",age :Int = 2019){
println("name = $name,age = $age ")
}
@JvmStatic
将对象声明或伴生对象的方法或属性的访问器暴露成一个同名的Java静态方法。
查看源码:
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
public actual annotation class JvmStatic
对于Kotlin的对象声明和伴生对象,在Kotlin中可以像静态函数那样调用类名.方法名进行调用。但在Java中,需要在这其中添加多一个Companion
或INSTANCE
,使调用很不自然。使用@JvmStatic
注解标记伴生对象或对象声明中的方法和属性,使其在Java中可以像Kotlin一样调用这些方法和属性。
在Kotlin中定义一个伴生对象,并用标记@JvmStatic
注解:
class daqi{
companion object {
@JvmStatic
val name:String = ""
@JvmStatic
fun daqiFunc(){
}
}
}
反编译可以观察到 伴生对象类 或 对象声明类 中声明了属于它们自己的方法和属性,但同时在对象声明类本身或伴生对象类的外部类中也声明了一样的静态的方法和属性访问器供外部直接访问。
public final class daqi {
@NotNull
private static final String name = "";
public static final daqi.Companion Companion = new daqi.Companion((DefaultConstructorMarker)null);
@NotNull
public static final String getName() {
daqi.Companion var10000 = Companion;
return name;
}
@JvmStatic
public static final void daqiFunc() {
Companion.daqiFunc();
}
public static final class Companion {
@JvmStatic
public static void name$annotations() {
}
@NotNull
public final String getName() {
return daqi.name;
}
@JvmStatic
public final void daqiFunc() {
}
}
}
所以,如果对象声明和伴生对象需要和Java层进行比较频繁的交互时,建议还是加上@JvmStatic
@JvmSuppressWildcards 和 @JvmWildcard
@JvmSuppressWildcards
指示编译器为泛型参数生成或省略通配符。(默认是省略)
@JvmWildcard
指示编译器为为泛型参数生成通配符。
查看源码:
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY, AnnotationTarget.TYPE)
@Retention(AnnotationRetention.BINARY)
@MustBeDocumented
public actual annotation class JvmSuppressWildcards(actual val suppress: Boolean = true)
--------------------------------------------------------------------------
@Target(AnnotationTarget.TYPE)
@Retention(AnnotationRetention.BINARY)
@MustBeDocumented
public actual annotation class JvmWildcard
@PurelyImplements
指示Kotlin编译器将带该注释的Java类视为给定Kotlin接口的纯实现。“Pure”在这里表示类的每个类型参数都成为该接口的非平台类型参数。
查看源码:
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
public annotation class PurelyImplements(val value: String)
Kotlin对来自Java的变量会当作平台类型来处理,由开发者觉得其是可空还是非空。但即便将其声明为非空,但其实他还是能接收空值或者返回空值。
#java文件
class MyList<T> extends AbstractList<T> { ... }
#kotlin文件
MyList<Int>().add(null) // 编译通过
但可以借助@PurelyImplements
注解,并携带对应的Kotlin接口。使其与Kotlin接口对应的类型参数不被当作平台类型来处理。
#java文件
@PurelyImplements("kotlin.collections.MutableList")
class MyPureList<T> extends AbstractList<T> { ... }
MyPureList<Int>().add(null) // 编译不通过
MyPureList<Int?>().add(null) // 编译通过
@Throws
等价于Java的throws关键字
查看源码:
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER, AnnotationTarget.CONSTRUCTOR)
@Retention(AnnotationRetention.SOURCE)
public annotation class Throws(vararg val exceptionClasses: KClass<out Throwable>)
例子
@Throws(IOException::class)
fun daqi() {
}
@Strictfp
等价于Java的strictfp关键字
查看源码:
@Target(FUNCTION, CONSTRUCTOR, PROPERTY_GETTER, PROPERTY_SETTER, CLASS)
@Retention(AnnotationRetention.SOURCE)
@MustBeDocumented
public actual annotation class Strictfp
@Transient
等价于Java的transient关键字
查看源码:
@Target(FIELD)
@Retention(AnnotationRetention.SOURCE)
@MustBeDocumented
public actual annotation class Transient
@Synchronized
等价于Java的synchronized关键字
查看源码:
@Target(FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER)
@Retention(AnnotationRetention.SOURCE)
@MustBeDocumented
public actual annotation class Synchronized
@Volatile
等价于Java的volatile关键字
查看源码:
@Target(FIELD)
@Retention(AnnotationRetention.SOURCE)
@MustBeDocumented
public actual annotation class Volatile
点目标声明
许多情况下, Kotlin代码中的单个声明会对应成多个 Java 声明 ,而且它们每
个都能携带注解。例如, Kotlin 属性就对应了 Java 宇段、 getter ,以
及一个潜在的 setter。这时需要使用点目标指定说明注解用在什么地方。
点目标声明被用来说明要注解的元素。使用点目标被放在@符号和注解名
称之间,并用冒号和注解名称隔开。
点目标的完整列表如下:
- property————Java 的注解不能应用这种使用点目标
- field————为属性生成的字段
- get ————属性的 getter
- set ————属性的 setter
- receiver ————扩展函数或者扩展属性的接收者参数。
- param————构造方法的参数。
- setparam————属性 setter 的参数
- delegate ————为委托属性存储委托实例的字段
- file ———— 包含在文件中声明的顶层函数和属性的类。
//注解的是get方法,而不是属性
@get:daqiAnnotation
val daqi:String = ""
@Target(AnnotationTarget.PROPERTY_GETTER)
annotation class daqiAnnotation()
参考资料:
- 《Kotlin实战》
- Kotlin官网