Kotlin知識歸納(十三) —— 註解

大棋發表於2019-07-20

前序

      註解是什麼?簡單說註解就是一種標註(標記、標識),沒有具體的功能邏輯程式碼。通過註解開發人員可以在不改變原有程式碼和邏輯的情況下在原始碼中嵌入補充資訊。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知識歸納(十三) —— 註解

      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具有良好的互通性,定義了一系列註解用於攜帶一些額外資訊,以便編譯器做相容轉換。

Kotlin知識歸納(十三) —— 註解

@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類中:

Kotlin知識歸納(十三) —— 註解

@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 ")
}
複製程式碼

Kotlin知識歸納(十三) —— 註解

@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知識歸納(十四) —— 反射

Kotlin知識歸納(十三) —— 註解

相關文章