1. 程式人生 > >Java 之路 (十九) -- 註解(語法、預定義註解、元註解、重複註解、註解與反射)

Java 之路 (十九) -- 註解(語法、預定義註解、元註解、重複註解、註解與反射)

前言

官方註解的定義如下:

註解(一種元資料形式)提供有關不屬於程式本身的程式的資料。註解對它們註解的程式碼的操作沒有直接影響。

註解有許多用途,其中包括:

  • 編譯器的資訊 - 編譯器可以使用註解來檢測錯誤或抑制警告。
  • 編譯時和部署時處理 - 軟體工具可以處理註解資訊以生成程式碼,XML檔案等。
  • 執行時處理 - 可以在執行時檢查某些註解。

第一次看時候覺得”這尼瑪是什麼”,於是換個簡單一點的說法。

我們可以把註解當作是“標籤”,就像是我們對某個人的看法一樣,這個“標籤”包含了對其中屬性的評價(註解元素)。我們就可以對某個程式元素貼上這種”標籤”(使用註解),既然能貼上去,那麼我們自然也能通過某種方式(反射)來將這個”標籤”撕下去(獲取註解)。

有了這種比喻,相信下面對於註解的講解就能更更容易理解了。一旦覺得理解不了註解是個什麼東西,就回想“註解 ≈ 標籤”就好了。

1. 基礎知識

1.1 註解的格式

  1. 註解中無元素:

    @Entity
    
    //如下,用於重寫的 @Override 註解
    @Override
    void myMethod(){...}
  2. 註解中有多個元素,且這些元素有值:

    //這裡的 @Author 是假定的自定義註解
    @Author(
      name = "Benjamin Franklin",
      date = "3/27/2003"
    )
    class MyClass() { ... }
  3. 註解中只有一個元素時:

    //@SuppressWarnings 該註解是 Java 預定義的註解
    @SuppressWarnings(value = "unchecked") void myMethod() { ... } //或者 可以將 value 省略 @SuppressWarnings("unchecked") void myMethod() { ... }
  4. 可以在同一宣告中新增多個註解:

    @Author(name = "Jane Doe")
    @EBook
    class MyClass { ... }
  5. 可以使用重複註解(使用相同型別的註解)

    @Author(name = "Jane Doe")
    @Author(name = "John Smith")
    class MyClass { ... }

1.2 可以使用註解的位置

註解可以應用於宣告以下內容:

  • 欄位
  • 方法
  • 其他程式元素

在 Java SE 8 以前,註解只能應用於宣告。從 Java SE 8開始,添加了型別註解,具體示例如下:

  • 建立類例項

    new @Interned MyObject();
  • 型別轉換

    myString = (@NonNull String) str;
  • implements 子句

    class UnmodifiableList<T> implements @Readonly List<@Readonly T> { ... }
  • 異常丟擲語句

    void monitorTemperature() throws @Critical TemperatureException { ... }

2. 宣告註解型別

註解型別定義類似於介面定義,通過使用 @interface 關鍵字進行定義。

@interface MyAnnotation{

}

前面我們提到過,註解中可以有元素,註解元素是通過以下方式定義的:

@interface MyAnnotation{
    int id() default 0;
    String msg();
    //...
}

這樣定義好之後,我們就可以使用該型別的註解,並填入值,如下例子:

@MyAnnotation(id = 3, msg = "hello annotation")
public class Test {
    //...
}

在定義 @MyAnnotation 註解時,我使用了一個關鍵字 default,它是用來指定預設值的。當指定預設值之後,在我們使用註解並不填入值時,Java 會自動將其設為預設值。於是我們就可以如下呼叫:

@MyAnnotation(msg = "hello annotation")
public class Test {
    //...
}

這裡需要注意註解元素的型別是有限制的,註解元素只能使用如下型別:

  • 基本資料型別
  • String
  • Class
  • enum
  • Annotation
  • 以上型別的陣列

3. 預定義註解

3.1 Java 使用的預定義註解

Java 使用的註解型別有如下幾種:

  1. @Deprecated

    表示被註解的元素已棄用,不應再使用,只要程式使用帶有 @Deprecated 註解的方法/類/欄位,編譯器就會生成警告。當不推薦使用時,也應使用 Javadoc @deprecated 對其標記。

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

    該註解通知編譯器被註解的元素要覆蓋超類中宣告的元素,具體實際上就是重寫方法時使用。

      // 將方法標記為已將超類方法覆蓋
      @Override 
      int overriddenMethod() { }

    需要強調一點:實際上,重寫方法時 @Override 並非必須的,之所以使用該註解是為了防止出錯。

    當使用 @Override 註解的方法並沒有正確覆蓋其超類方法時,編譯器會報錯。

  3. @SuppressWarnings

    該註解告知編譯器禁止可能生成的特定警告。舉個例子,下面的示例中,使用了啟用的方法,通常編譯器會生成警告,但是使用 @SuppressWarnings 註解後,會導致該警告被禁止生成。

    @SuppressWarnings("deprecation")
       void useDeprecatedMethod() {
           // deprecation warning
           // - suppressed
           objectOne.deprecatedMethod();
       }

    關於編譯器警告,Java 語言規範指定了兩個類別:deprecationunchecked

    deprecation:使用了棄用方法時的類別

    unchecked:當與泛型出現之前編寫的遺留程式碼進行互動時,可能出現的警告類別。

  4. @SafeVarargs

    當該註解應用於方法或建構函式時,它保證了不會對可變引數執行不安全的操作。當使用該註解後,有關 可變引數的 unchecked 警告將被禁止生成。

  5. @FunctionalInterface

    在Java SE 8中引入的,指出型別宣告的目的是作為一個函式式介面註解

3.2 元註解

適用於其他註解的註解稱為元註解

  1. @Retention

    指定註解的儲存方式:

    1. RetentionPolicy.SOURCE - 標記的註解僅保留在源級別中,並被編譯器忽略。
    2. RetentionPolicy.CLASS - 標記的註解在編譯時由編譯器保留,但Java虛擬機器(JVM)會忽略。
    3. RetentionPolicy.RUNTIME - 標記的註解由JVM保留,因此執行時環境可以使用它。
  2. @Documented

    作用是能夠將註解中的元素包含到 Javadoc 中去

  3. @Target

    限制可以應用註解的 Java 元素型別:

    • ElementType.ANNOTATION_TYPE可以應用於註解型別。

    • ElementType.CONSTRUCTOR可以應用於建構函式。

    • ElementType.FIELD可以應用於欄位或屬性。
    • ElementType.LOCAL_VARIABLE可以應用於區域性變數。
    • ElementType.METHOD可以應用於方法級註釋。
    • ElementType.PACKAGE可以應用於包宣告。
    • ElementType.PARAMETER可以應用於方法的引數。
    • ElementType.TYPE可以應用於類的任何元素。
  4. @Inherited

    表明註解型別可以從超類繼承。當超類使用了被” @Inherited 註解了的” 註解後,如果它的子類沒有新增任何註解,那麼子類會繼承超類的註解。

    比較繞,舉個例子:

    @Inherited
    @interface Test {}
    
    
    @Test
    public class A {}
    
    
    public class B extends A {}

    上面,@Test 註解是可以被繼承的,A 使用了 @Test 註解,B 繼承 A,且未新增任何註解,於是 B 也就擁有 @Test 這個註解。

  5. @Repeatable

    Java SE 8 加入的特性,表明被註解的註解可以多次應用於相同的宣告。

    具體參見 重複註解 小節。

4. 重複註解

Java SE 8 中,加入了重複註解,這使得我們可以將相同的註解應用於宣告或型別引用。

它的宣告包含兩個步驟:

  1. 宣告可重複的註解型別
  2. 宣告包含的註解型別

下面通過一個例子來介紹具體如何宣告和使用重複註解:

第1步:宣告可重複的註解型別

註解型別必須使用 @Repeatable 元註解進行標記:

@Repeatable(Schedules.class)
public @interface Schedule {
  String dayOfMonth() default "first";
  String dayOfWeek() default "Mon";
  int hour() default 12;
}

其中, @Repeatable 元註解的值是Java編譯器為儲存重複註解而生成的容器註解的型別。此例中,包含的註解型別是 Schedules,因此 @Schedule 註解儲存在 @Schedules 註解中。

第2步:宣告包含的註解型別

包含的註解型別必須帶有陣列型別的 value 元素,陣列的型別必須是可重複的註解型別:

public @interface Schedules {
    Schedule[] value();
}

5. 檢索 (Retrieving) 註解 - 註解與反射

前面我們所有講解的都是如何宣告和使用註解,那麼此處就來講講怎麼獲取註解。

這主要是通過 反射 實現的,主要涉及以下 AnnotatedElement 介面提供的抽象方法 :

  1. 查詢是否應用了某個註解

    default boolean isAnnotationPresent(Class<? extends Annotation> annotationClass)
  2. 獲取註解

方法 功能 說明
<T extends Annotation> T getAnnotation(Class<T> annotationClass) 返回該程式元素上存在的指定型別的註解,如果該型別的註解不存在,則返回 null 泛型引數表明,只能說註解型別或者某註解型別的子類
Annotation[] getAnnotations() 返回該元素上存在的所有註解,包括繼承得到的。如果沒有註解,那麼返回長度為零的陣列
default <T extends Annotation> T[] getAnnotationsByType(Class<T> annotationClass) Java SE 8 新增方法,用來獲得修飾該程式元素的、指定型別的多個註解
default <T extends Annotation> T getDeclaredAnnotation(Class<T> annotationClass) 返回直接修飾該程式元素、指定型別的註解。如果該型別註解不存在,返回 null 忽略繼承得到的註解
default <T extends Annotation> T[] getDeclaredAnnotationsByType(Class<T> annotationClass) Java SE 8 新增方法,用來獲得直接修飾該程式元素的、指定型別的多個註解 忽略繼承得到的註解
Annotation[] getDeclaredAnnotations() 返回直接存在於此元素上的所有註解。如果沒有註解,返回長度為零的陣列。 忽略繼承得到的註解

下面具體來看個例子:

MyAnnotation.java

@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)//指定註解儲存在JVM 中,執行時可使用
@Target({ElementType.TYPE,ElementType.FIELD,ElementType.METHOD})//註解可以應用於類、方法和欄位上
public @interface MyAnnotation{ 
    String type() default "ignore";
    String[] hobby();
}

TestAnnotation.java

@MyAnnotation(type = "class",hobby = {"sleep","play"})
public class TestAnnotation {

    @MyAnnotation(type = "Field",hobby = {"read"})
    private String whdalive;

    @MyAnnotation(type = "Field",hobby = {"piano"})
    private String cy;

    @MyAnnotation(type = "Method",hobby = {"guitar"})
    public void method1(){

    }

    public static void main(String[] args) {

        //反射獲取類物件
        Class<TestAnnotation> clz = TestAnnotation.class;
        //類上是否含有指定型別註解
        boolean clzHasAnno = clz.isAnnotationPresent(MyAnnotation.class);
        //如果有,獲取註解元素的值
        if(clzHasAnno) {
            MyAnnotation annotation = clz.getAnnotation(MyAnnotation.class);
            String type = annotation.type();
            String[] hobby = annotation.hobby();
            System.out.println(clz.getName() + ", type = " + type + ", hobby = " + Arrays.asList(hobby).toString());
        }
        //反射獲取欄位
        Field[] fields = clz.getDeclaredFields();
        for(Field field : fields) {
            boolean fieldHasAnno = field.isAnnotationPresent(MyAnnotation.class);
            if (fieldHasAnno) {
                MyAnnotation annotation = field.getAnnotation(MyAnnotation.class);
                String type = annotation.type();
                String[] hobby = annotation.hobby();
                System.out.println(field.getName() + ", type = " + type + ", hobby = " + Arrays.asList(hobby).toString());
            }
        }
        //反射獲取方法
        Method[] methods = clz.getDeclaredMethods();
        for (Method method : methods) {
            boolean methodHasAnno = method.isAnnotationPresent(MyAnnotation.class);
            if (methodHasAnno) {
                MyAnnotation annotation = method.getAnnotation(MyAnnotation.class);
                String type = annotation.type();
                String[] hobby = annotation.hobby();
                System.out.println(method.getName() + ", type = " + type + ", hobby = " + Arrays.asList(hobby).toString());
            }

        }
    }
}

輸出結果

com.whdalive.reflection.TestAnnotation, type = class, hobby = [sleep, play]
whdalive, type = Field, hobby = [read]
cy, type = Field, hobby = [piano]
method1, type = Method, hobby = [guitar]

總結

註解是 Java 引入的一個非常受歡迎的機制,它提供了一種結構化的並且具有型別檢查能力的新途徑。我們可以通過註解為程式碼加入元資料,而不會導致程式碼雜亂難以閱讀。

同時,註解還是 Android 諸多開源框架的實現基礎。比如 ButterKnife、Retrofit、Dagger2 等等等等。

註解還是很有必要好好學習一下的。

共勉。

相關推薦

Java () -- 註解語法預定註解註解重複註解註解反射

前言 官方註解的定義如下: 註解(一種元資料形式)提供有關不屬於程式本身的程式的資料。註解對它們註解的程式碼的操作沒有直接影響。 註解有許多用途,其中包括: 編譯器的資訊 - 編譯器可以使用註解來檢測錯誤或抑制警告。 編譯

Java () -- 內部類概念分類特性意義"多重繼承"繼承

1. 內部類基礎 1.1 什麼是內部類 內部類的定義如下: 可以將一個類的定義放在另一個類的定義內部,這就是內部類 更具體一點,對於程式設計思想而言: 內部類允許將邏輯相關的類組織在一起,並控制位於內部的類的可視性。 內部類就像是一種程式碼隱

Java (一) -- 持有物件CollectionListSetQueueMapIteratorforeach

本章將簡單介紹一下常用的集合類的特點,同時並不會深入原始碼分析原理,本文目的僅僅在於對 Java 集合類有一個整體認識 關於 API,本文不涉及過多,建議直接檢視 Java 官方文件 1. 容器概述 1.1 引入原因 Java 中,陣列用

Java (五) -- 泛型上泛型類泛型方法有界型別引數泛型繼承型別推斷

Thinking in Java 中關於泛型的講解篇幅實在過長,前後嘗試閱讀這一章,但總是覺得找不到要點,很迷。於是放棄 Thinking in Java 泛型一章的閱讀,轉而官方教程,本章可以算作官方教程的中文版。 1.為什麼使用泛型 簡單來說

Java (六) -- 泛型下萬用字元型別擦除泛型的限制

7. 萬用字元 萬用字元,即 “?”,用來表示未知型別。 萬用字元可用作各種情況:作為引數,欄位或區域性變數的型別;有時也作為返回型別;萬用字元從不用作泛型方法呼叫、泛型類例項建立或超型別的型別引數。 7.1 上限有界的萬用字元 使用上限萬用字元來放

JAVA學習第java程序的異常處理

num 函數 錯誤 style col 編譯失敗 return [] java 異常處理的捕捉形式: 這是能夠對異常進行針對性處理的方式 六、try、catch的理解 詳細格式: try { //須要被檢測異常的代碼 } catch(異常類 變量)//改變量用

小白的JAVA——泛型講解

從本小結開始,我們就要開始理解泛型的原理咯。在學習之前,我們先要了解知識,以便幫助我們更好的理解泛型。 泛型程式碼和虛擬機器:虛擬機器是沒有泛型物件的,所有物件都是屬於普通類的,因此虛擬機器在解析泛型程式碼時,會產生一個相應的原始型別。 原始型別:  每當我們定義一個泛型

小白的JAVA——泛型講解

由於我學習java的時候跳過了UI開發的知識學習,因此對於元件Compoent類會有點陌生,不過這並不影響我們講解泛型機制的原理,現在,讓我們來繼續學習泛型機制。 4.1 呼叫遺留程式碼 java中的遺留程式碼是指編寫於JAVA SE5.0之前的程式碼,一直遺留到現在。那

cocos2dx學習----第深入理解單點觸控的事件機制

上一篇我們簡單接觸了關於單點觸控的實現。 這一篇我們繼續進一步的理解單點觸控的事件分發的優先順序問題。 我們來回顧一下實現觸控的過程: 1.建立新的監聽器; 2.為新的監聽器分配回撥函式(而我們在上一篇中用到了Lamda表示式來實現); 3.註冊分發監聽器。 好,這一篇就是

測開:實現棧

ima 9.png 進行 bsp 需要 線性表 之路 一個數 src 棧: 棧作為一種數據結構,是一種只能在一端進行插入和刪除操作的特殊線性表。它按照先進後出的原則存儲數據,先進入的數據被壓入棧底,最後的數據在棧頂,需要讀數據的時候從棧頂開始彈出數據(最後一個數據

Java (二) -- Java I/O 上BIO檔案資料流如何選擇I/O流典型用例

前言 Java 的 I/O 類庫使用 流 這個抽象概念,代表任何有能力產出資料的資料來源物件或者是有能力接收資料的接收端物件。流 遮蔽了實際的 I/O 裝置中處理資料的細節。 資料流是一串連續不斷的資料的集合,簡單理解的話,我們可以把 Java 資料流當作是

#Java學習——基礎階段二四篇

out 出現 萬能 -c ack 分隔 status osi 版本 我的學習階段是跟著CZBK黑馬的雙源課程,學習目標以及博客是為了審查自己的學習情況,畢竟看一遍,敲一遍,和自己歸納總結一遍有著很大的區別,在此期間我會參雜Java瘋狂講義(第四版)裏面的內容。 前言:此隨

JVM菜鳥進階高手基礎知識開場白

由於 重要性 基礎 陌生 bsp 參數 高手之路 開發人員 基礎知識 轉載請註明原創出處,謝謝! 最近沒有什麽實戰,準備把JVM知識梳理一遍,先以開發人員的交流來談談jvm這塊的知識以及重要性,依稀記得2、3年前用solr的時候老是經常oom,提到oom大家應該都不陌生,那

javajava學習-01-Linux基礎

x文件 字母 at命令 超過 用戶登錄 創建刪除 軟連接 nbsp tail linux學習方法: 你的程序要在服務器(linux)上執行,服務器沒有桌面系統,學習linux就是學習命令。 一、Linux介紹 1、芬蘭大學生,名字叫Linux,因為個人興趣,編寫了一個類Un

學無止盡,.Net開發者學習Java,開篇

項目 width 很多 maven 安裝jdk 比較 一律 uget 分享 Visual Studio是最好的開發工具,沒有之一 最近有空,學習學習以前丟掉的Java內容,已經沒有印象了 寫這篇文章的目的主要是學習Java,當然也是給自己學習Java的鼓鼓勁,萬一哪天就

工作那些事談談碼農農民工區別和發展 工作那些事如果哪一天,沒有了電腦 工作那些事十三再次失業

工作那些事系列連結快速通道,不斷更新中: 工作那些事(一)今年工作不好找 工作那些事(二)應聘時填寫個人資訊ABCD 工作那些事(三)什麼樣的公司能吸引你,什麼樣的公司適合你? 工作那些事(四)大公司VS小公司 工作那些事(五)談談專案資料整理和積累 工作那些事(六)談談

ElasticSearch學習筆記 Java REST Client

ElasticSearch學習筆記之二十九 Java REST Client Java REST Client Java High Level REST Client Compatibility(相容性) Javadoc Maven Reposi

開啟JAVA--語法篇-1

學了幾年的C/C++,今天開始正式轉學JAVA,希望能在這條路上越走越深,越來越好~先從語法開始~擼一擼JAVA核心技術。 1、java種每個方法都是包含在類中的,包括main方法,main方法一般定義為public static void main,其為靜態函式,說明

JOKER_的JAVAJava概念的匯入及開發環境的搭建

一.java 第一個概念 1. JDK(Java Development Kit) 是 Java 語言的軟體開發工具包(SDK)。 2.JRE(Java Runtime Environment)縮寫,指Java執行環境 二.java開發環境的搭建 (對比之

Java:合併序列

SequenceInputStream類可以將多個輸入流按順序連線起來。SequenceInputStream的構造方法是使用一對輸入流或者一個輸入流的列舉(內含多個輸入流)作為引數。 //將輸入流s1和s2合併 SequenceInputStream(InputStrea