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

  • 2019 年 10 月 3 日
  • 筆記

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();          }        }  }

程序運行結果:

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