用影片《記憶碎片》來解釋Java注解的工作原理

注解于我們而言并不陌生,遺憾的是,大多數人對于注解的認識,都只停留在使用的層面上,對其背后的原理則知之甚少。

在享受注解所帶來的便利的同時,你是否也曾發出過這樣的疑問,即:

小小的一個注解,是怎么幫我們完成某件特定的工作的呢?

大家好,我是碼仔,今天我們要分享的主題是Java注解的工作原理

在文章開始之前需要先說明的是,本期我們將采用一種比較新穎的講解方式,即類比的手法,這種手法在我們平時接觸和學習一門全新的事物時經常運用。

不同之處在于,本期要類比的一系列事物,來源于一部經典的影視作品——由克里斯托弗·諾蘭導演、蓋·皮爾斯主演的懸念影片《記憶碎片》。

記憶碎片

毫無疑問這需要讀者老爺們看過這部影片,且對影片中的主要情節有大概的認知。沒看過的也不要緊,下面會提供一個劇情梗概,以便我們快速掌握該影片的一些背景知識和劇情設定。

劇情梗概

影片的主人公萊尼在一次與入室搶劫歹徒的搏斗中身負重傷,妻子也慘遭殺害。雖然萊尼僥幸活了下來,但卻從此患上了一種十分奇特的“短期失憶癥”,只能記住受傷之前以及當前最多十分鐘之內的事情。

因不滿于警方的草草結案,萊尼誓要自己追查到兇手并替愛妻報仇,但支離破碎的記憶卻使萊尼舉步維艱。他只能不斷地借助紙條、照片、紋身上的筆記來記錄有價值的線索,告訴自己下一步的目標,因為很可能十分鐘后,他就完全記不得自己在哪里,要做什么了。

讓我們來提取一下其中的關鍵詞:短期失憶癥、復仇、筆記。這里著重介紹一下短期失憶癥這個重要設定。

打個比方,就像是我們的App被禁止了往磁盤里寫入新的數據,往后的通信都只能依賴內存和舊有的磁盤數據。由于內存不是持久化存儲,因此每次App重啟后,之前存儲在內存的那部分數據就丟失了,App被迫又回到了之前的初始狀態。

了解完影片的劇情梗概,我們再來對注解的概念有一個基本的認識。

注解是什么?

官方文檔上對于注解(Annotation)的解釋如下:

注解是一種元數據形式,提供了與程序相關、但不屬于程序本身的數據。注解對它所注解的代碼的操作沒有直接影響。

嗯…這個措辭可以說是很官方、很專業性了,就是讀完之后,不免和記憶剛重啟的萊尼一樣一臉困惑。

我們提取一下關鍵的內容重新組織一下:

  1. 注解提供了一些數據用于解釋程序。
  2. 注解并不會影響程序本身的運行。

什么意思呢?我們可以用影片中的重要道具——萊尼的筆記來進行類比。

筆記是萊尼用于應對短期失憶癥的道具,解釋了萊尼當前所處的地方是在哪里,以及出現在這里的目的,但筆記本身并不會給萊尼疊加什么力量或攻速的Buff。

筆記要真正發揮作用,是需要萊尼在記憶重啟后主動地去檢查并嘗試梳理之后才可以。

注解也是一樣,它只提供數據,并不影響程序,真正要依靠注解完成某個功能,還需要我們有一個主動檢索注解的步驟。

但在檢索之前,我們需要先完成注解的定義與基本應用。

注解的定義與基本應用

想象你就是萊尼本尼,對你來說:

注解的定義,就相當于你每次構思筆記內容的過程;

而注解的應用,則相當于你將其寫到紙條、照片或紋到身體的某一處的過程。

回到注解本身。

要定義一個注解,最簡單的方式中如下:

public @interface Entity {
}

如你所見,其與接口的定義方式很相似,區別在于interface關鍵字前面多加了一個@符號,用于向編譯器指示這是一個注解

注解的基本應用也很簡單,在類、字段、方法等元素的聲明前面加上@Xxx即可。根據Java的習慣,每個注解通常要占據單獨一行。

@Entity
class MyClass { ... }

不過,光這樣還不夠,要讓想注解真正起作用,我們還需要為注解添加上元注解。

元注解是什么?

元注解是應用于其他注解之上的注解。

這樣說有點拗口,你可以這樣理解:

元注解本身也是一個注解,只不過其作用的對象限定為了另外一個注解。

就像萊尼的筆記也必須遵循敘事的六要素(時間/地點/人物等)一樣,元注解的作用,就是注明了一個注解對象必須包含的基本要素,比如保留時間、作用對象等。

Java內部定義了幾種元注解類型:

@Retention

Retention從字面上理解是保持、保留的意思,當@Retention被應用到一個注解之上時,即注明了這個注解的的保留時間。

用影片中的一個情節來舉例就是:萊尼在吉米的衣服口袋里找到了一條寫在杯墊底部的筆記,筆記指示去菲迪斯酒吧找娜塔莉。@Retention元注解就相當于給這條筆記指定了有效時間為“找到娜塔莉為止”,找到娜塔莉之后該筆記就過期失效了。

又比如,萊尼在影片開頭自述,說他會把認為重要的事情直接紋在身上,以作為永久備忘。像這一類的筆記,相當于用@Retention元注解指定了筆記的有效時間為”永久“,其將在每次記憶重啟后都作為關鍵線索使用。

理解之后,我們在來看再來看@Retention元注解可能的取值:

  • RetentionPolicy.SOURCE – 注解只在源碼階段保留,編譯時將被忽略。
  • RetentionPolicy.CLASS – 注解只被保留到編譯階段,但會被JVM忽略。
  • RetentionPolicy.RUNTIME – 注解由JVM保留,因此可以在運行階段使用。

保留到不同階段的注解,有著各自不同的作用,這個我們放到后面再講。

@Documented

這個元注解的作用,是將其修飾的注解包含到Javadoc中去。

@Target

Target這個單詞我們都認識,是目標、靶子的意思,它限定了注解可以應用于哪種Java元素。

這次我們可以用萊尼寫在人物照片前后的筆記來類比:

照片1正面寫著“泰迪”,背面寫著“別相信他的謊言”。@Target元注解就相當于照片正面的人物名字,限定了筆記作用到的目標人物為“泰迪”。

照片2正面寫著“娜塔莉”,背面寫著“她也失去了愛人,會同情你、幫你”,@Target元注解同樣相當于限定了筆記作用到的目標人物為“娜塔莉”。

@Target元注解可能的取值如下:

  • ElementType.ANNOTATION_TYPE 可以給一個注解進行注解
  • ElementType.CONSTRUCTOR 可以給構造方法進行注解
  • ElementType.FIELD 可以給屬性進行注解
  • ElementType.LOCAL_VARIABLE 可以給局部變量進行注解
  • ElementType.METHOD 可以給方法進行注解
  • ElementType.PACKAGE 可以給一個包進行注解
  • ElementType.PARAMETER 可以給一個方法內的參數進行注解
  • ElementType.TYPE 可以給一個類型進行注解,比如類、接口、枚舉

@Inherited

Inherited是繼承的意思,但并不是說注解本身可以被繼承,而是說如果一個父類被一個包含@Inherited元注解的注解所修飾,那么它的子類如果沒有包含任何注解的話,就默認繼承了該父類的這個注解。

比如,我們為前一小節的@Entity注解添加@Inherited元注解后,重新應用到MyClass類,之后定義一個MyClass的子類SubClass,那么SubClass默認也將擁有@Entity這個注解:

@Inherited
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Entity {
}
@Entity
class MyClass { ... }
class SubClass extends MyClass {...}

Java 7之后,又添加多了3個新類型的元注解@SafeVarargs、@FunctionalInterface、@Repeatable,感興趣的可以去了解一下,這里就不一一展開了。

注解的屬性

如果說,元注解指定的是一個注解必須包含的部分,那么關于注解可自定義擴展的部分,則是由注解的屬性來指定的。

注解的屬性是以“無形參方法”的形式來聲明的,其方法名定義了該屬性的名字,返回值定義了該屬性的類型,可選的類型包括幾種基本數據類型外加字符串、類、枚舉、注解及它們的數組。

屬性可以有默認值,用default關鍵字指定。

比如以下代碼,就為@Author注解聲明了2個String類型的屬性:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface Author {
   String author() default "unknown";
   String date();
}

然后,在為類、方法、字段等元素添加@Author注解時,就可以為這2個屬性賦值:

@Author(
   name = "Benjamin Franklin",
   date = "3/27/2003"
)
class MyClass { ... }

另外,如果注解只有一個名為value的屬性,那么可以省略該名稱:

@SuppressWarnings("unchecked")
void myMethod() { ... }

而如果注解不包含任何屬性,則連括號都可以省略了:

@Override
void mySuperMethod() { ... }

注解的提取

在編譯Java源代碼時,注解可以交由一個叫做注解處理器的編譯器插件進行處理。處理器可以生成信息,或創建額外的Java源文件或資源,這些文件或資源又可以反過來再被編譯和處理。

這個該怎么理解呢?

有一個情節是這樣的,萊尼在制服了達德之后拍下照片,并根據在自己身上摸索出的紙條上面的內容,重新梳理了下一步目標并記錄在照片上。

這里的紙條就相當于交給處理器的注解,是一條線索,照片就相當于根據注解額外創建的Java源文件或資源,反過來又可以作為下一步的線索。

除了可以使用注解處理器來處理注解外,由于注解類型和類一樣,都會被編譯并存儲在字節碼文件(.class)中,因此我們還可以自己編寫代碼,使用反射來處理注解。

從Java SE 5開始,與反射相關的java.lang.reflect軟件包就為注解定義了一系列的新接口,在Class、Constructor、Field、Method和Package中都有對應的實現,主要的方法有:

  • isAnnotationPresent(Class<? extends Annotation> annotationType) :判斷該Java元素是否應用了某個注解

  • getAnnotation(Class<T> annotationClass):獲取某個指定類型的注解

  • getAnnotations() :返回這個Java元素上的所有注解

合理利用這幾個方法,我們就可以在運行時動態判斷指定的Java元素是否包含某個注解,以及根據提取到的注解內容,編寫對應的處理邏輯,完成某件特定的工作。

提取操作的演示代碼將在《定義并運用自定義注解》一節中給出。

注解的作用

以上內容都掌握了之后,我們再回過頭來,講解保留到不同階段的注解的作用:

RetentionPolicy.SOURCE

只在源碼階段保留的注解,通常是起代替代碼注釋的作用。

比如有開發團隊會要求在開始對每個類的正式編寫之前,必須以注釋的形式提供這個類的重要信息。

public class Generation3List extends Generation2List {

   // Author: John Doe
   // Date: 3/17/2002
   // Current revision: 6
   // Last modified: 4/12/2004
   // By: Jane Doe
   // Reviewers: Alice, Bill, Cindy

   // class code goes here

}

我們可以改由注解的形式來實現,為此,我們需要先定義一個注解類型:

@interface ClassPreamble {
   String author();
   String date();
   int currentRevision() default 1;
   String lastModified() default "N/A";
   String lastModifiedBy() default "N/A";
   // Note use of array
   String[] reviewers();
}

然后,就可以在對應類的前面添加該注解,并為該注解的各項屬性賦值。

@ClassPreamble (
   author = "John Doe",
   date = "3/17/2002",
   currentRevision = 6,
   lastModified = "4/12/2004",
   lastModifiedBy = "Jane Doe",
   // Note array notation
   reviewers = {"Alice", "Bob", "Cindy"}
)
public class Generation3List extends Generation2List {

// class code goes here

}

我們還可以搭配@Documented元注解,使得該注解包含的信息出現在Javadoc生成的文檔中。

RetentionPolicy.CLASS

保留到編譯階段的注解,主要有以下兩個作用:

  1. 提供信息給編譯器——編譯器可以使用注解來檢測錯誤或抑制警告。
  2. 編譯階段時的處理——軟件工具可以用來處理注解信息以生成代碼、XML文件等。

作用1,我們將在《內置注解》一節中講到。

作用2,我們將以EventBus框架為例來說明。

EventBus從2.X到3.X最大的變化,就是引入了注解處理器,以解決原先反射獲取性能較低的問題。該處理器會在構建時,檢索所有注解并生成一個類,該類會包含所有在運行時需要的數據,也就是說耗時的工作都在編譯階段完成了,因而極大地提高了運行階段的處理速度。

RetentionPolicy.RUNTIME

保留到運行階段的注解,可以在程序運行的時候接受代碼的提取,以實現動態處理——這是我們最常規的用法。

定義并運用自定義注解

如果你讀到這里,恭喜你已經掌握了自定義一個注解所需要具備的所有知識了,下面就讓我們來實際操作一下,提取一個類注解的數據:

步驟1,定義一個名為TypeHeader的注解,指定保留到運行時:

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

// This is the annotation to be processed
// Default for Target is all Java Elements
// Change retention policy to RUNTIME (default is CLASS)
@Retention(RetentionPolicy.RUNTIME)
public @interface TypeHeader {
    // Default value specified for developer attribute
    String developer() default "Unknown";
    String lastModified();
    String [] teamMembers();
    int meaningOfLife();
}

步驟2,將注解應用與某個類上,并為注解聲明的各項屬性賦值:

// This is the annotation being applied to a class
@TypeHeader(developer = "Bob Bee",
    lastModified = "2013-02-12",
    teamMembers = { "Ann", "Dan", "Fran" },
    meaningOfLife = 42)

public class SetCustomAnnotation {
    // Class contents go here
}

步驟3,獲取該類的Class對象,先調用Class對象的isAnnotationPresent方法,判斷是否存在@TypeHeader注解;如果存在,再調用getAnnotation方法獲取@TypeHeader注解并打印注解的屬性:

// This is the example code that processes the annotation
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;

public class UseCustomAnnotation {
    public static void main(String [] args) {
        Class<SetCustomAnnotation> classObject = SetCustomAnnotation.class;
        readAnnotation(classObject);
    }

    static void readAnnotation(AnnotatedElement element) {
        try {
            System.out.println("Annotation element values: \n");
            if (element.isAnnotationPresent(TypeHeader.class)) {
                // getAnnotation returns Annotation type
                Annotation singleAnnotation = 
                        element.getAnnotation(TypeHeader.class);
                TypeHeader header = (TypeHeader) singleAnnotation;

                System.out.println("Developer: " + header.developer());
                System.out.println("Last Modified: " + header.lastModified());

                // teamMembers returned as String []
                System.out.print("Team members: ");
                for (String member : header.teamMembers())
                    System.out.print(member + ", ");
                System.out.print("\n");

                System.out.println("Meaning of Life: "+ header.meaningOfLife());
            }
        } catch (Exception exception) {
            exception.printStackTrace();
        }
    }
}

內置注解

除了可以自定義注解,Java API本身也內置了幾個現成可用的注解,這里列舉幾個常見的:

@Deprecated

這個注解用于表示其所標記的元素已被棄用,不應再使用。每當程序使用帶有@Deprecated注解的方法、類或字段時,編譯器都會生成警告。

通常還要搭配Javadoc的@deprecated標簽進行記錄,解釋其為什么被棄用:

   // Javadoc comment follows
    /**
     * @deprecated
     * explanation of why it was deprecated
     */
    @Deprecated
    static void deprecatedMethod() { }
}

@Override

這個注解用于通知編譯器,其所標記的元素旨在覆蓋父類中聲明的元素,比如方法、字段等:

   // mark method as a superclass method
   // that has been overridden
   @Override 
   int overriddenMethod() { }

雖然我們在重寫方法時,并沒要求必須使用此注解,但它有助于防止錯誤情況的發生。比如被標記為@Override的方法如果在父類中實際不存在,編譯器將提示錯誤。

@SuppressWarnings

這個注解用于讓編譯器抑制特定的警告。比如當我們使用了Java API不建議使用的方法(比如被棄用的方法)時,編譯器就會生成警告。而當我們在該方法前添加@SuppressWarnings注解后,該警告就會被抑制:

   // use a deprecated method and tell 
   // compiler not to generate a warning
   @SuppressWarnings("deprecation")
    void useDeprecatedMethod() {
        // deprecation warning
        // - suppressed
        objectOne.deprecatedMethod();
    }

簡直是強迫癥患者的福音了。

好了,以上就是今天要分享的內容,現在我們可以來回答開篇的那個問題了:

  • 注解只是提供了數據,本身并不會做任何事情。因此,單純添加注解,并不會影響程序的運行;
  • 真正要依靠注解完成某個功能,還須得有一個主動檢索注解的步驟;
  • 檢索注解就是一個提取注解自定義屬性的過程,根據提取結果的不同編寫對應的處理邏輯代碼;
  • 檢索注解的時機由@Retention元注解決定,該元注解指定了其修飾的注解將保留到哪個階段;
  • 保留到編譯階段,則是交由了注解處理器處理;
  • 保留到運行階段,則是利用反射機制進行提取。

少俠,請留步!若本文對你有所幫助或啟發,還請:

  1. 點贊????,讓更多的人能看到!
  2. 收藏??,好文值得反復品味!
  3. 關注?,不錯過每一次更文!

===> 技術號:「星際碼仔」??

你的支持是我繼續創作的動力,感謝!??

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,333評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,491評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,263評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,946評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,708評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,409評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,939評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,774評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,641評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,872評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,650評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373

推薦閱讀更多精彩內容