阅读 84

Kotlin知识归纳(十三) —— 注解

前序

      注解是什么?简单说注解就是一种标注(标记、标识),没有具体的功能逻辑代码。通过注解开发人员可以在不改变原有代码和逻辑的情况下在源代码中嵌入补充信息。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个元注解,分别是:MustBeDocumentedRepeatableRetentionTarget

@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代码中无法使用TargetAnnotationTarget.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中,需要在这其中添加多一个CompanionINSTANCE,使调用很不自然。使用@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()
复制代码

参考资料:

android Kotlin系列:

Kotlin知识归纳(一) —— 基础语法

Kotlin知识归纳(二) —— 让函数更好调用

Kotlin知识归纳(三) —— 顶层成员与扩展

Kotlin知识归纳(四) —— 接口和类

Kotlin知识归纳(五) —— Lambda

Kotlin知识归纳(六) —— 类型系统

Kotlin知识归纳(七) —— 集合

Kotlin知识归纳(八) —— 序列

Kotlin知识归纳(九) —— 约定

Kotlin知识归纳(十) —— 委托

Kotlin知识归纳(十一) —— 高阶函数

Kotlin知识归纳(十二) —— 泛型

Kotlin知识归纳(十三) —— 注解

Kotlin知识归纳(十四) —— 反射

关注下面的标签,发现更多相似文章
评论