Java注解深入理解

內容概要

  • Annotation的概念

  • Annotation的作用

  • Annotation的分類

  • 系統內置注解

  • 元注解

  • 自定義注解

  • 解析注解信息

  • JDK8注解新特性

附:項目源碼地址

一、Annotation的概念

Annotation(注解)是插入代碼中的元數據,在JDK5.0及以后版本引入。它可以在編譯期使用預編譯工具進行處理, 也可以在運行期使用 Java 反射機制進行處理,用于創建文檔,跟蹤代碼中的依賴性,甚至執行基本編譯時檢查。因為本質上,Annotion是一種特殊的接口,程序可以通過反射來獲取指定程序元素的Annotion對象,然后通過Annotion對象來獲取注解里面的元數據。(元數據從metadata一詞譯來,就是“關于數據的數據”的意思)

二、Annotation的作用

Annotation的作用大致可分為三類:

  • 編寫文檔:通過代碼里標識的元數據生成文檔;

  • 代碼分析:通過代碼里標識的元數據對代碼進行分析;

  • 編譯檢查:通過代碼里標識的元數據讓編譯器能實現基本的編譯檢查;

綜上所述可知,Annotation主要用于提升軟件的質量和提高軟件的生產效率。

三、Annotation的分類

  • 根據成員個數分類

1.標記注解:沒有定義成員的Annotation類型,自身代表某類信息,如:@Override

2.單成員注解:只定義了一個成員,比如@SuppressWarnings 定義了一個成員String[] value,使用value={...}大括號來聲明數組值,一般也可以省略“value=”

3.多成員注解:定義了多個成員,使用時以name=value對分別提供數據

  • 根據注解使用的功能和用途分類

1.系統內置注解:系統自帶的注解類型,如@Override

2.元注解:注解的注解,負責注解其他注解,如@Target

3.自定義注解:用戶根據自己的需求自定義的注解類型

四、系統內置注解

  • JavaSE中內置三個標準注解,定義在java.lang中

1.@Override:用于修飾此方法覆蓋了父類的方法;

2.@Deprecated:用于修飾已經過時的方法;

3.@SuppressWarnnings:用于通知java編譯器禁止特定的編譯警告;

  • @Override 限定重寫父類方法

@Override 是一個標記注解類型,它被用作標注方法。它說明了被標注的方法重載了父類的方法,起到了斷言的作用。如果我們使用了這種Annotation在一個沒有覆蓋父類方法的方法時,java編譯器將以一個編譯錯誤來警示。

下面的代碼是一個使用@Override修飾一個企圖重載父類的displayName()方法,而又存在拼寫錯誤成displayname(),這時編譯器就會提示錯誤:

public class Fruit{
    public void displayName(){
        System.out.println("水果的名字是:*****");
    }
}

class Orange extends Fruit{
    @Override
    public void displayName(){
        System.out.println("水果的名字是:桔子");
    }
}

class Peach extends Fruit{
    @Override
    public void displayname(){
        System.out.println("水果的名字是:桃子");
    }
}

Orange 類編譯不會有任何問題,Peach 類在編譯的時候會提示相應的錯誤;@Override注解只能用于方法,不能用于其他程序元素。

  • @Deprecated 用于標記已過時

Deprecated也是一個標記注解。當一個類型或者類型成員使用@Deprecated修飾的話,編譯器將不鼓勵使用這個被標注的程序元素。而且這種修飾具有一定的 “延續性”:如果我們在代碼中通過繼承或者覆蓋的方式使用了這個過時的類型或者成員,雖然繼承或者覆蓋后的類型或者成員并不是被聲明為 @Deprecated,但編譯器仍然要報警。

注意,@Deprecated這個annotation類型和javadoc中的 @deprecated這個tag是有區別的:前者是java編譯器識別的,而后者是被javadoc工具所識別用來生成文檔。

下面一段程序中使用了@Deprecated注解標示方法過期,同時在方法注釋中用@deprecated tag 標示該方法已經過時,代碼如下:

public class AppleService {
    public void displayName(){
        System.out.println("水果的名字是:蘋果");
    }

    /**
     * @deprecated 該方法已經過期,不推薦使用
     */
    @Deprecated
    public void showTaste(){
        System.out.println("水果的蘋果的口感是:脆甜");
    }

    public void showTaste(int typeId){
        if(typeId==1){
            System.out.println("水果的蘋果的口感是:酸澀");
        }
        else if(typeId==2){
            System.out.println("水果的蘋果的口感是:綿甜");
        }
        else{
            System.out.println("水果的蘋果的口感是:脆甜");
        }
    }
}

public class AppleConsumer {
    //@SuppressWarnings({"deprecation"})
    public static void main(String[] args) {
        AppleService appleService=new AppleService();
        appleService.showTaste();
        appleService.showTaste(2);
    }
}

AppleService類的showTaste() 方法被@Deprecated標注為過時方法,在AppleConsumer類中使用的時候,編譯器會給出該方法已過期,不推薦使用的提示。

  • @SuppressWarnnings 抑制編譯器警告

@SuppressWarnings 其注解目標為類、字段、函數、函數入參、構造函數和函數的局部變量。在java5.0,sun提供的javac編譯器為我們提供了-Xlint選項來使編譯器對合法的程序代碼提出警告,此種警告從某種程度上代表了程序錯誤。例如當我們使用一個generic collection類而又沒有提供它的類型時,編譯器將提示出"unchecked warning"的警告。通常當這種情況發生時,我們就需要查找引起警告的代碼。如果它真的表示錯誤,我們就需要糾正它。例如如果警告信息表明我們代碼中的switch語句沒有覆蓋所有可能的case,那么我們就應增加一個默認的case來避免這種警告。

有時我們無法避免這種警告,例如,我們使用必須和非generic的舊代碼交互的generic collection類時,我們不能避免這個unchecked warning。此時@SuppressWarning就要派上用場了,在調用的方法前增加@SuppressWarnings修飾,告訴編譯器停止對此方法的警告。

SuppressWarning不是一個標記注解。它有一個類型為String[]的成員,這個成員的值為被禁止的警告名。使用示例如下:

public class SuppressWarningTest {
    @SuppressWarnings("unchecked")
    public void addItems2(String item){
        @SuppressWarnings("unused")
        List list = new ArrayList();
        List items = new ArrayList();
        items.add(item);
    }

    @SuppressWarnings({"unchecked","unused"})
    public void addItems1(String item){
        List list = new ArrayList();
        list.add(item);
    }

    @SuppressWarnings("all")
    public void addItems(String item){
        List list = new ArrayList();
        list.add(item);
    }
}

@SuppressWarnings注解的常見參數值:

1.deprecation:使用了不贊成使用的類或方法時的警告;

2.unchecked:執行了未檢查的轉換時的警告,例如當使用集合時沒有用泛型 (Generics) 來指定集合保存的類型;

3.fallthrough:當 Switch 程序塊直接通往下一種情況而沒有 Break 時的警告;

4.path:在類路徑、源文件路徑等中有不存在的路徑時的警告;

5.serial:當在可序列化的類上缺少 serialVersionUID 定義時的警告;

6.finally:任何 finally 子句不能正常完成時的警告;

7.unused:代碼中的變量或方法沒有被使用產生的警告;

8.rawtypes:使用泛型時沒有指定類型的警告;

9.all:關于以上所有情況的警告。

10.更多關鍵字

五、元注解

元注解的作用就是負責注解其他注解。Java5.0定義了4個標準的meta-annotation類型,它們被用來提供對其它 annotation類型作說明。Java5.0定義的元注解:

1.@Target

2.@Retention

3.@Documented

4.@Inherited

  • @Target

作用:描述該注解修飾的范圍,可被用于 packages、types(類、接口、枚舉、Annotation類型)、類型成員(方法、構造方法、成員變量、枚舉值)、方法參數和本地變量(如循環變量、catch參數)。

取值(ElementType):

1.CONSTRUCTOR:用于描述構造器

2.FIELD:用于描述域

3.LOCAL_VARIABLE:用于描述局部變量

4.METHOD:用于描述方法

5.PACKAGE:用于描述包

6.PARAMETER:用于描述參數

7.TYPE:用于描述類、接口(包括注解類型) 或enum聲明

  • @Retention

作用:描述該注解的生命周期,表示在什么編譯級別上保存該注解的信息。Annotation被保留的時間有長短:某些Annotation僅出現在源代碼中,而被編譯器丟棄;而另一些卻被編譯在class文件中;編譯在class文件中的Annotation可能會被虛擬機忽略,而另一些在class被裝載時將被讀?。ㄕ堊⒁獠⒉挥绊慶lass的執行,因為Annotation與class在使用上是被分離的)。

取值(RetentionPoicy):

1.SOURCE:在源文件中有效(即源文件保留)

2.CLASS:在class文件中有效(即class保留)
    
3.RUNTIME:在運行時有效(即運行時保留)

  • @Documented

@Documented Annotation的作用是在生成javadoc文檔的時候將該Annotation也寫入到文檔中。

  • @Inherited

作用:@Inherited 元注解是一個標記注解,@Inherited闡述了某個被標注的類型是被繼承的。如果一個使用了@Inherited修飾的annotation類型被用于一個class,則這個annotation將被用于該class的子類。

六、自定義注解

使用@interface自定義注解,自動繼承了java.lang.annotation.Annotation接口,由編譯程序自動完成其他細節。在定義注解時,不能繼承其他的注解或接口。@interface用來聲明一個注解,其中的每一個方法實際上是聲明了一個配置參數。方法的名稱就是參數的名稱,返回值類型就是參數的類型(返回值類型只能是基本類型、Class、String、enum)??梢酝ㄟ^default來聲明參數的默認值。

  • 定義注解格式:
    public @interface 注解名 {定義體}

  • 注解參數的可支持數據類型:

1.所有基本數據類型(int,float,boolean,byte,double,char,long,short)

2.String類型

3.Class類型

4.enum類型

5.Annotation類型

6.以上所有類型的數組

  • 參數定義要點

1.只能用public或默認(default)這兩個訪問權修飾;

2.參數成員只能用基本類型byte,short,char,int,long,float,double,boolean八種基本數據類型和 String,Enum,Class,annotations等數據類型,以及這一些類型的數組;

3.如果只有一個參數成員,建議參數名稱設為value();

4.注解元素必須有確定的值,要么在定義注解的默認值中指定,要么在使用注解時指定,非基本類型的注解元素的值不可為null。因此, 使用空字符串或負數作為默認值是一種常用的做法。

  • 簡單的自定義注解實例:
/**
 *自定義注解MyAnnotation
 */
@Target(ElementType.TYPE) //目標對象是類型
@Retention(RetentionPolicy.RUNTIME) //保存至運行時
@Documented //生成javadoc文檔時,該注解內容一起生成文檔
@Inherited //該注解被子類繼承
public @interface MyAnnotation {
    public String value() default ""; //當只有一個元素時,建議元素名定義為value(),這樣使用時賦值可以省略"value="
    String name() default "devin"; //String
    int age() default 18; //int
    boolean isStudent() default true; //boolean
    String[] alias(); //數組
    enum Color {GREEN, BLUE, RED,} //枚舉類型
    Color favoriteColor() default Color.GREEN; //枚舉值
}


@MyAnnotation(
        value = "info",
        name = "myname",
        age = 99,
        isStudent = false,
        alias = {"name1", "name2"},
        favoriteColor = MyAnnotation.Color.RED
)
public class MyClass {
    //使用MyAnnotation注解,該類生成的javadoc文檔包含注解信息如下:
    /*
    @MyAnnotation(value = "info", name = "myname", age = 99, isStudent = false, alias = {"name1","name2"}, favoriteColor = Color.RED)
    public class MyClass
    extends Object
     */
}


public class MySubClass extends MyClass{
    //子類MySubClass繼承了父類MyClass的注解
}

七、解析注解信息

Java使用Annotation接口來代表程序元素前面的注解,該接口是所有Annotation類型的父接口。相應地,Java在java.lang.reflect 包下新增了AnnotatedElement接口,該接口代表程序中可以接受注解的程序元素。

實際上,java.lang.reflect 包所有提供的反射API擴充了讀取運行時Annotation信息的能力。當一個Annotation類型被定義為運行時的Annotation后,該注解才能是運行時可見,當class文件被裝載時被保存在class文件中的Annotation才會被虛擬機讀取。

AnnotatedElement接口是所有程序元素(Field、Method、Package、Class和Constructor)的父接口,所以程序通過反射獲取了某個類的AnnotatedElement對象之后,程序就可以調用該對象的如下七個方法來訪問Annotation信息:

1.<T extends Annotation> T getAnnotation(Class<T> annotationClass) :返回該程序元素上存在的、指定類型的注解,如果該類型注解不存在,則返回null;

2.Annotation[] getDeclaredAnnotation(Class<T>):返回該程序元素上存在的、指定類型的注解,如果該類型注解不存在,則返回null;與此接口中的其他方法不同,該方法將忽略繼承的注解;

3.Annotation[] getAnnotations():返回該程序元素上存在的所有注解;

4.Annotation[] getDeclaredAnnotations():返回直接存在于此元素上的所有注釋。與此接口中的其他方法不同,該方法將忽略繼承的注解;

5.Annotation[] getAnnotationsByType(Class<T>):返回直接存在于此元素上指定注解類型的所有注解;

6.Annotation[] getDeclaredAnnotationsByType(Class<T>):返回直接存在于此元素上指定注解類型的所有注解。與此接口中的其他方法不同,該方法將忽略繼承的注解;

7.boolean isAnnotationPresent(Class<?extends Annotation> annotationClass):判斷該程序元素上是否包含指定類型的注解,存在則返回true,否則返回false;

/***********注解聲明***************/
/**
 * 水果名稱注解
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FruitName {
    String value() default " ";
}


/**
 * 水果顏色注解
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FruitColor {
    /**
     * 顏色枚舉
     */
    public enum Color{BLUE, RED, GREEN};

    /**
     * 顏色屬性
     * @return
     */
    Color fruitColor() default Color.GREEN;
}


/**
 * 水果供應商注解
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FruitProvider {
    /**
     * 供應商編號
     * @return
     */
    public int id() default -1;

    /**
     * 供應商名稱
     * @return
     */
    public String name() default " ";

    /**
     * 供應商地址
     * @return
     */
    public String address() default " ";
}
/***********注解使用***************/
public class Apple {
    @FruitName("Apple")
    private String appleName;
    @FruitColor(fruitColor = FruitColor.Color.RED)
    private String appleColor;
    @FruitProvider(id = 1, name = "陜西紅富士集團", address = "陜西紅富士大廈")
    private String appleProvider;

    public String getAppleProvider() {
        return appleProvider;
    }

    public void setAppleProvider(String appleProvider) {
        this.appleProvider = appleProvider;
    }

    public String getAppleName() {
        return appleName;
    }

    public void setAppleName(String appleName) {
        this.appleName = appleName;
    }

    public String getAppleColor() {
        return appleColor;
    }

    public void setAppleColor(String appleColor) {
        this.appleColor = appleColor;
    }

    public void displayName(){
        System.out.println(getAppleName());
    }
}

/***********注解信息獲取***************/
public class AnnotationParser {
    public static void main(String[] args) {
        Field[] fields = Apple.class.getDeclaredFields();
        for (Field field : fields) {
            //System.out.println(field.getName().toString());
            if (field.isAnnotationPresent(FruitName.class)){
                FruitName fruitName = field.getAnnotation(FruitName.class);
                System.out.println("水果的名稱:" + fruitName.value());
            }else if (field.isAnnotationPresent(FruitColor.class)){
                FruitColor fruitColor = field.getAnnotation(FruitColor.class);
                System.out.println("水果的顏色:"+fruitColor.fruitColor());
            }else if (field.isAnnotationPresent(FruitProvider.class)){
                FruitProvider fruitProvider = field.getAnnotation(FruitProvider.class);
                System.out.println("水果供應商編號:" + fruitProvider.id() + " 名稱:" + fruitProvider.name() + " 地址:" + fruitProvider.address());
            }
        }
    }
}

/***********輸出結果***************/
水果的名稱:Apple
水果的顏色:RED
水果供應商編號:1 名稱:陜西紅富士集團 地址:陜西紅富士大廈

八、JDK8注解新特性

JDK 8 主要有兩點改進:類型注解和重復注解

  • 類型注解

類型注解在@Target中增加了兩個ElementType參數:

1.ElementType.TYPE_PARAMETER 表示該注解能寫在類型變量的聲明語句中;

2.ElementType.TYPE_USE 表示該注解能寫在使用類型的任何語句中(例如聲明語句、泛型和強制轉換語句中的類型);

從而擴展了注解使用的范圍,可以使用在創建類實例、類型映射、implements語句、throw exception聲明中的類型前面。例如:

1.創建類實例

new @Interned MyObject();

2.類型映射

myString = (@NonNull String) str;

3.implements 語句中

class UnmodifiableList<T> implements @Readonly List<@Readonly T> { ... }

4.throw exception聲明

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

簡單示例:

@Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
public @interface Encrypted {
}


public class MyTypeAnnotation {
    @Encrypted String data;
    List<@Encrypted String> strings;
}

類型注解的作用:

首先,局域變量聲明中的類型注解也可以保留在類文件中,完整泛型被保留,并且在運行期可以訪問,從而有助于我們獲取更多的代碼信息;其次,類型注解可以支持在的程序中做強類型檢查。配合第三方工具check framework,可以在編譯的時候檢測出runtime error,以提高代碼質量;最后,代碼中包含的注解清楚表明了編寫者的意圖,使代碼更具有表達意義,有助于閱讀者理解程序,畢竟代碼才是“最根本”的文檔、“最基本”的注釋。

  • 重復注解

重復注釋就是運行在同一元素上多次使用同一注解,使用@Repeatable注解。

之前也有重復使用注解的解決方案,但可讀性不是很好,例如:

public @interface Authority {
     String role();
}

public @interface Authorities {
    Authority[] value();
}

public class RepeatAnnotationUseOldVersion {    
    @Authorities({@Authority(role="Admin"),@Authority(role="Manager")})
    public void doSomeThing(){
    }
}

而現在的實現如下:

@Repeatable(Authorities.class)
public @interface Authority {
     String role();
}

public @interface Authorities {
    Authority[] value();
}

public class RepeatAnnotationUseNewVersion {
    @Authority(role="Admin")
    @Authority(role="Manager")
    public void doSomeThing(){ }
}

不同的地方是,創建重復注解Authority時,加上@Repeatable,指向存儲注解Authorities,在使用時候,直接可以重復使用Authority注解。從上面例子看出,java 8里面做法更適合常規的思維,可讀性強一點。

總結

本篇主要從Annotation的概念、作用、分類進行了大概的介紹,然后通過對系統內置注解、元注解、自定義注解、解析注解信息等四個方面逐步以代碼實例的方式展開對注解認識和使用,最后講了JDK8中新添加的類型注解和重復注解,從而對Java 注解有了更系統化的認識。從jdk5引入注解以來,我們看到了注解在javadoc文檔、JUnit單元測試、Spring依賴配置等各方面蓬勃發展,而現在jdk8更是大大拓展了注解的使用范圍,為新的設計和工具帶來了更多的機遇,讓我們拭目以待。希望本篇對剛入門或想對注解有個全面回顧的小伙伴有幫助,更多關于Java的精彩內容,敬請關注DevinBlog

參考文章:

Java Annotations

深入理解Java注解

Java8新特性探究

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

推薦閱讀更多精彩內容