1. 程式人生 > >夯實Java基礎(十七)——註解(Annotation)

夯實Java基礎(十七)——註解(Annotation)

1、註解概述

從JDK5.0開始,Java增加對元資料(MetaData)的支援,也就是註解(Annotation)。其實我們早就已經接觸過註解了,例如我們經常在Java程式碼中可以看到 “@Override”,“@Test”等等這樣的東西,它們就是Java中的註解。註解可以像修飾符一樣使用,可以用於修飾包、類、構造器、方法、成員變數、引數、區域性變數的宣告。

我們需要注意的是,註解與註釋是有一定區別的,註解就是程式碼裡面的特殊標記,這些標記可以在編譯,類載入,執行時被讀取,並執行相應的處理。通過註解開發人員可以在不改變原有程式碼和邏輯的情況下在原始碼中嵌入補充資訊。而註釋則是用以說明某段程式碼的作用,或者說明某個類的用途、某個方法的功能和介紹,以及該方法的引數和返回值的資料型別及意義等等。

2、Java內建註解

在JavaSE部分,註解的使用往往比較簡單,Java中提供了5個內建註解,它們分別是:

①、@Override:標註該方法是重寫父類中的方法。

這個註解一個是我們見得最多的一個了,提示這個方法是重寫於父類的方法。

②、@Deprecated:標記某個功能已經過時,用於定義過時的類、方法、成員變數等。

這個註解想必大家應該都有碰到過,在使用Date日期類的時候,裡面有大量過時的方法,我們來定義一個Date類來呼叫一個方法。

這個getDay()方法就是過時的,我們點選進去看一下這個方法的原始碼:

果然這個方法是用@Deprecated修飾過的。同時也可以發現我們在呼叫過時元素時,編譯器在編譯階段遇到這個註解時會發出提醒警告,告訴開發者正在呼叫一個過時的元素,當然如果不想看到警告我們可以抑制它的出現。

③、@SuppressWarnings:抑制編譯器警告。

上面說到用@Deprecated修飾過的元素在呼叫時會有警告,我們可以用@SuppressWarnings註解來抑制警告的出現。

可以發現左邊的警告沒有了。@SuppressWarnings這個註解中引數非常的多,這裡介紹幾個常見的引數:

  • all:抑制所有警告。
  • deprecation:抑制過期方法警告。
  • null:忽略對null的操作。
  • unchecked:抑制沒有進行型別檢查操作的警告。
  • unused:抑制沒被使用過的程式碼的警告。

如果需要了解更多的可以去檢視官方文件。

④、@FunctionaInterface:指定介面必須為函式式介面。

這個註解是Java8出現的新特性。這個函式式介面的意思就是介面中有一個且僅有一個抽象方法,但是可以有多個非抽象方法,如果不定義或定義多個抽象方法就會報錯。

正式因為JDK 8中lambda表示式的引入,使得函式式介面在Java中變得越來越流行。因為這些特殊型別的介面可以用lambda表示式、方法引用或建構函式引用輕鬆替換。

⑤、@SafeVarargs:抑制"堆汙染警告"。

這個註解是在Java7中引入,主要目的是處理可變長引數中的泛型,此註解告訴編譯器:在可變長引數中的泛型是型別安全的。可變長引數是使用陣列儲存的,而陣列和泛型不能很好的混合使用。因為陣列元素的資料型別在編譯和執行時都是確定的,而泛型的資料型別只有在執行時才能確定下來,因此當把一個泛型儲存到陣列中時,編譯器在編譯階段無法檢查資料型別是否匹配,因此會給出警告資訊。

我們來看下面這個示例:

public class Test {
    @SafeVarargs//這裡告訴編譯器型別安全,不讓有警告。其實方法體內容型別不安全
    public static void show(List<String>...lists){
        Object[] arry=lists;
        List<Integer> intList=Arrays.asList(11,22,33);
        arry[0]=intList;//這裡就是堆汙染,這裡沒有警告,是因為只針對於可變長引數泛型
        String str=lists[0].get(0);//java.lang.ClassCastException
    }

    public static void main(String[] args) {
        List<String> list1=Arrays.asList("AA","BB","CC");
        List<String> list2=Arrays.asList("DD","EE","DD");
        show(list1,list2);
    }
}

通過上述的示例,我們將intList賦給array[0],array[0]的型別是List<String>,但是儲引用到實際為List<Integer>型別的值,這個無效的引用被稱為堆汙染。由於直到執行時才能確定此錯誤,因此它會在編譯時顯示為警告,這裡沒有警告,是因為只針對於可變長引數泛型,並在執行時出現ClassCastException。

注意:@SafeVarargs註解只能用在引數長度可變的方法或構造方法上,且方法必須宣告為static或final,否則會出現編譯錯誤。

3、自定義註解

我們在享受註解給我們帶來方便地同時,我們自己應該要知道怎麼去定義註解。註解的自定義非常的簡單,通過 @interface關鍵字進行定義,可以發現這個關鍵字和介面interface很相似,就在前面加了一個 @符號,但是它和介面沒有任何關係。自定義註解還需要注意的一點是:所有的自定義註解都自動繼承了java.lang.annotation.Annotation這個介面。自定義註解的格式:

public @interface 註解名 {
       //屬性
}    

同樣我們可以在註解中定義屬性,它的定義有點類似於方法,但又不是方法,在註解中是不能宣告普通方法的。註解的屬性在註解定義中以無引數方法的形式來宣告,其方法名定義了屬性的名字,其返回值定義了該屬性的型別,我們稱為配置引數。它們的型別只能是八種基本資料型別、String型別、Class型別、enum型別、Annotation型別以上所有型別的陣列。例如:

//定義了一個MyAnnotation註解
public @interface MyAnnotation {
    String[] value();
}

@MyAnnotation(value = "hello")
class Test{

}

上面註解程式碼中,定義了一個String的value陣列。然後我們在使用的時候,就可以使用 屬性名稱=“xxx” 的形式賦值。

註解中屬性還可以有預設值,預設值需要用 default 關鍵值指定。比如:.

//定義了一個MyAnnotation註解
public @interface MyAnnotation {
    String id();
    String[] value() default {"AA","BB"};
}

@MyAnnotation(id="one")
class Test{

}

上面定義了 id 屬性沒有預設值,而value屬性中則設定了預設值,所以在使用註解的時候只需給 id 屬性賦值即可,value可以不用寫。

通過以上形式自定義的註解暫時都還沒有任何實用的價值,因為自定義註解必須配上註解的資訊處理流程(使用反射)才有意義。如何讓註解真真的發揮作用,主要就在於註解處理方法,所以接下來我們將學習元註解和註解的反射。

4、元註解

元註解就是用來修飾其他註解的註解。我們隨便點進一個註解的原始碼都可以發現有元註解。

Java5.0中定義了4個標準的元註解型別,它們被用來提供對其它註解型別作說明:

  • @Retention
  • @Target
  • @Documented
  • @Inherited

而Java8.0中又增加了一個新的元註解型別:

  • @Repeatable

所以接下來我們將逐個分析它們的作用和使用方法。

1、@Retention:用於指定該Annotation的生命週期。

這個元註解只能用於修飾一個Annotation定義,它的內部包含了一個RetentionPolicy列舉型別的屬性,而這個列舉類中定義了三個列舉例項,SOURCE、CLASS、RUNTIME。它們各個值的意思如下:

  • RetentionPolicy.SOURCE:在原始檔中有效(即原始檔保留),在編譯器進行編譯時它將被丟棄忽視。
  • RetentionPolicy.CLASS:在class檔案中有效(即class保留),當Java程式執行時,它並不會被載入到 JVM 中,只保留在class檔案中。這個是預設值。
  • RetentionPolicy.RUNTIME:在執行時有效(即執行時保留),當Java程式執行時,註解會被載入進入到 JVM 中,所以我們可以使用反射獲取到它們。

 

比較典型的是@SuppressWarnings註解,如果我們用 javap -c去反編譯它是看到這個註解的,因為在編譯的時候就已經被丟棄了。

②、@Target:用於指定該Annotation能夠用在哪些地方。

@Target內部定義了一個列舉型別的陣列ElementType[] value(),在ElementType這個列舉類中引數有很多,我們來看一下:

  • TYPE:用於描述類、介面(包括註解型別) 或enum宣告
  • FIELD:用於描述域即類成員變數
  • METHOD:用於描述方法
  • PARAMETER:用於描述引數
  • CONSTRUCTOR:用於描述構造器
  • LOCAL_VARIABLE:用於描述區域性變數
  • ANNOTATION_TYPE:由於描述註解型別
  • PACKAGE:用於描述包
  • TYPE_PARAMETER:1.8版本開始,描述類、介面或enum引數的宣告
  • TYPE_USE:1.8版本開始,描述一種類、介面或enum的使用宣告

③、@Document:表示Annotation可以被包含到javadoc中去。預設情況下javadoc是不包含註解的。

由於這個比較簡單所以不細說。

④、@Inherited:被它修飾的Annotation將具有繼承性。

@Inherited修飾過的Annotation其子類會自動具有該註解。在實際應用中,使用情況非常少。

⑤、@Repeatable:用於指示它修飾的註解型別是可重複的。

這個註解是在Java8中新出的特性,說到這個可重複註解可能有點不理解。我們通過示例來理解一下:

先定義一個MyAnnotation註解:

@Inherited
@Documented
@Repeatable(MyAnnotations.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.FIELD,ElementType.METHOD,ElementType.PARAMETER})
public @interface MyAnnotation {
    String value() default "Hello";
}

這裡需要說明@Repeatable(MyAnnotations.class),它表示在同一個類中@MyAnnotation註解是可以重複使用的,重複的註解被存放至@MyAnnotations註解中。

然後再定義一個MyAnnotations註解:

@Inherited
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.FIELD,ElementType.METHOD,ElementType.PARAMETER})
public @interface MyAnnotations {
    MyAnnotation[] value();
}

這個MyAnnotations註解裡面的屬性必須要宣告為要重複使用註解的型別陣列MyAnnotation[] value();,而且相應的生命週期和使用地方都需要同步。否則就會編譯報錯!

進行測試:

@MyAnnotation(value = "World")
@MyAnnotation(value = "World")
public class Test{

}

而在Java8之前沒有@Repeatable註解是這樣寫的:

@MyAnnotations({@MyAnnotation(value = "World"),@MyAnnotation(value = "World")})
public class Test{

}

5、註解處理器(使用反射)

以上講的所以註解的定義都只是一個形式,實際上還並沒有什麼可使用的價值,而註解的核心就是使用反射來獲取到它們,所以下面我們要來學習註解處理器(使用反射)的使用。

下面參考:https://www.cnblogs.com/peida/archive/2013/04/26/3038503.html

java.lang.reflect 包下主要包含一些實現反射功能的工具類,實際上,java.lang.reflect 包所有提供的反射API擴充了讀取執行時Annotation資訊的能力。當一個Annotation型別被定義為RUNTIME的註解後,該註解才能是執行時可見,當class檔案被裝載時被儲存在class檔案中的Annotation才會被虛擬機器讀取。
AnnotatedElement 介面是所有程式元素(Class、Method和Constructor)的父介面,所以程式通過反射獲取了某個類的AnnotatedElement物件之後,程式就可以呼叫該物件的如下四個個方法來訪問Annotation資訊:

  • 方法1:<T extends Annotation> T getAnnotation(Class<T> annotationClass): 返回改程式元素上存在的、指定型別的註解,如果該型別註解不存在,則返回null。
  • 方法2:Annotation[] getAnnotations():返回該程式元素上存在的所有註解。
  • 方法3:boolean is AnnotationPresent(Class<?extends Annotation> annotationClass):判斷該程式元素上是否包含指定型別的註解,存在則返回true,否則返回false.
  • 方法4:Annotation[] getDeclaredAnnotations():返回直接存在於此元素上的所有註釋。與此介面中的其他方法不同,該方法將忽略繼承的註釋。(如果沒有註釋直接存在於此元素上,則返回長度為零的一個數組。)該方法的呼叫者可以隨意修改返回的陣列;這不會對其他呼叫者返回的陣列產生任何影響。

一個簡單的註解處理器:  

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)//這裡必須定義為RUNTIME
public @interface MyAnnotation {
    String id();
    String[] value() default {"AA","BB"};
}

@MyAnnotation(id = "hello")
class Test{
    public static void main(String[] args) {
        boolean annotationPresent = Test.class.isAnnotationPresent(MyAnnotation.class);
        System.out.println(annotationPresent);
        if ( annotationPresent ) {
            MyAnnotation myAnnotation = Test.class.getAnnotation(MyAnnotation.class);

            System.out.println("id:"+myAnnotation.id());
            System.out.println("value:"+ Arrays.toString(myAnnotation.value()));
        }
    }
}

程式執行結果:

上面的例子只是作用在類上面的註解,如果要作用在屬性、方法等上面的註解我們應該怎麼獲取呢?

定義作用於類上面的MyAnnotation註解:

//作用於類上面的註解
@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)//這裡必須定義為RUNTIME
public @interface MyAnnotation {
    String[] value() default "";
}

定義作用於屬性上面的AttributeAnnotation註解:

//作用於屬性上的註解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)//這裡必須定義為RUNTIME
public @interface AttributeAnnotation {
    String value();
}

定義作用於方法上面的MethodAnnotation註解:

//作用於方法上的註解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)//這裡必須定義為RUNTIME
public @interface MethodAnnotation {
    String value();
}

然後就是測試了:

@MyAnnotation(value = "MyAnnotation")
class Test{

    @AttributeAnnotation(value = "AttributeAnnotation")
    String str;

    @MethodAnnotation(value = "MethodAnnotation_show")
    public void show(){
        System.out.println("MethodAnnotation_show");
    }

    @MethodAnnotation(value = "MethodAnnotation_display")
    public void display(){
        System.out.println("MethodAnnotation_display");
    }

    public static void main(String[] args) {
        //獲取類上面的註解
        boolean annotationPresent = Test.class.isAnnotationPresent(MyAnnotation.class);
        System.out.println(annotationPresent);
        if ( annotationPresent ) {
            MyAnnotation myAnnotation = Test.class.getAnnotation(MyAnnotation.class);
            System.out.println("class-annotation:"+Arrays.toString(myAnnotation.value()));
        }

        try {
            //獲取單個屬性中的註解
            Field str = Test.class.getDeclaredField("str");
            AttributeAnnotation attributeAnnotation = str.getAnnotation(AttributeAnnotation.class);
            if (attributeAnnotation!=null){
                System.out.println("attribute-annotation:"+attributeAnnotation.value());
            }

            //獲取多個方法中的註解
            Method[] declaredMethods = Test.class.getDeclaredMethods();
            for (Method declaredMethod : declaredMethods) {
                    MethodAnnotation methodAnnotation = declaredMethod.getAnnotation(MethodAnnotation.class);
                    if (methodAnnotation!=null){
                        System.out.println("method-annotation:"+methodAnnotation.value());
                    }
            }

        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }

    }
}

程式執行結果:

小弟菜鳥只能領悟這麼多了,如果有錯誤或者需要補充的地方歡迎大家留言指出。謝謝!