Java核心知識點之註解簡介和最佳實踐

Java註解簡介

Annotation 中文譯過來就是註解、標釋的意思,在 Java 中註解是一個很重要的知識點,但經常還是有點讓新手不容易理解。

我個人認爲,比較糟糕的技術文檔主要特徵之一就是:用專業名詞來介紹專業名詞。 比如:

Java 註解用於爲 Java 代碼提供元數據。作爲元數據,註解不直接影響你的代碼執行,但也有一些類型的註解實際上可以用於這一目的。Java 註解是從 Java5 開始添加到 Java 的。這是大多數網站上對於 Java 註解,解釋確實正確,但是說實在話,我第一次學習的時候,頭腦一片空白。這什麼跟什麼啊?聽了像沒有聽一樣。因爲概念太過於抽象,所以初學者實在是比較喫力才能夠理解,然後隨着自己開發過程中不斷地強化練習,纔會慢慢對它形成正確的認識。

我在寫這篇文章的時候,我就在思考。如何讓自己或者讓讀者能夠比較直觀地認識註解這個概念?是要去官方文檔上翻譯說明嗎?我馬上否定了這個答案。

後來,我想到了一樣東西————墨水,墨水可以揮發、可以有不同的顏色,用來解釋註解正好。

不過,我繼續發散思維後,想到了一樣東西能夠更好地代替墨水,那就是印章。印章可以沾上不同的墨水或者印泥,可以定製印章的文字或者圖案,如果願意它也可以被戳到你任何想戳的物體表面。

但是,我再繼續發散思維後,又想到一樣東西能夠更好地代替印章,那就是標籤。標籤是一張便利紙,標籤上的內容可以自由定義。常見的如貨架上的商品價格標籤、圖書館中的書本編碼標籤、實驗室中化學材料的名稱類別標籤等等。

並且,往抽象地說,標籤並不一定是一張紙,它可以是對人和事物的屬性評價。也就是說,標籤具備對於抽象事物的解釋。

在這裏插入圖片描述
所以,基於如此,我完成了自我的知識認知升級,我決定用標籤來解釋註解。

註解如同標籤
之前某新聞客戶端的評論有蓋樓的習慣,於是 “喬布斯重新定義了手機、羅永浩重新定義了傻X” 就經常極爲工整地出現在了評論樓層中,並且廣大網友在相當長的一段時間內對於這種行爲樂此不疲。這其實就是等同於貼標籤的行爲。在某些網友眼中,羅永浩就成了傻X的代名詞。

廣大網友給羅永浩貼了一個名爲“傻x”的標籤,他們並不真正瞭解羅永浩,不知道他當教師、砸冰箱、辦博客的壯舉,但是因爲“傻x”這樣的標籤存在,這有助於他們直接快速地對羅永浩這個人做出評價,然後基於此,羅永浩就可以成爲茶餘飯後的談資,這就是標籤的力量。

而在網絡的另一邊,老羅靠他的人格魅力自然收穫一大批忠實的擁泵,他們對於老羅貼的又是另一種標籤。
在這裏插入圖片描述
老羅還是老羅,但是由於人們對於它貼上的標籤不同,所以造成對於他的看法大相徑庭,不喜歡他的人整天在網絡上評論抨擊嘲諷,而崇拜欣賞他的人則會願意掙錢購買錘子手機的發佈會門票。

我無意於評價這兩種行爲,我再引個例子。

《奇葩說》是近年網絡上非常火熱的辯論節目,其中辯手陳銘被另外一個辯手馬薇薇攻擊說是————“站在宇宙中心呼喚愛”,然後貼上了一個大大的標籤————“雞湯男”,自此以後,觀衆再看到陳銘的時候,首先映入腦海中便是“雞湯男”三個大字,其實本身而言陳銘非常優秀,爲人師表、作風正派、談吐舉止得體,但是在網絡中,因爲娛樂至上的環境所致,人們更願意以娛樂的心態來認知一切,於是“雞湯男”就如陳銘自己所說成了一個撕不了的標籤。

我們可以抽象概括一下,標籤是對事物行爲的某些角度的評價與解釋。

到這裏,終於可以引出本文的主角註解了。

初學者可以這樣理解註解:想像代碼具有生命,註解就是對於代碼中某些鮮活個體的貼上去的一張標籤。簡化來講,註解如同一張標籤。

在未開始學習任何註解具體語法而言,你可以把註解看成一張標籤。這有助於你快速地理解它的大致作用。如果初學者在學習過程有大腦放空的時候,請不要慌張,對自己說:

註解,標籤。註解,標籤。

什麼是註解?

對於很多初次接觸的開發者來說應該都有這個疑問?Annontation是Java5開始引入的新特徵,中文名稱叫註解。它提供了一種安全的類似註釋的機制,用來將任何的信息或元數據(metadata)與程序元素(類、方法、成員變量等)進行關聯。爲程序的元素(類、方法、成員變量)加上更直觀更明瞭的說明,這些說明信息是與程序的業務邏輯無關,並且供指定的工具或框架使用。Annontation像一種修飾符一樣,應用於包、類型、構造方法、方法、成員變量、參數及本地變量的聲明語句中。

Java註解是附加在代碼中的一些元信息,用於一些工具在編譯、運行時進行解析和使用,起到說明、配置的功能。註解不會也不能影響代碼的實際邏輯,僅僅起到輔助性的作用。包含在 java.lang.annotation 包中。

註解的用處:

1、生成文檔。這是最常見的,也是java 最早提供的註解。常用的有@param @return 等 2、跟蹤代碼依賴性,實現替代配置文件功能。比如Dagger 2依賴注入,未來java開發,將大量註解配置,具有很大用處; 3、在編譯時進行格式檢查。如@override 放在方法前,如果你這個方法並不是覆蓋了超類方法,則編譯時就能檢查出。

註解的原理:

註解本質是一個繼承了Annotation的特殊接口,其具體實現類是Java運行時生成的動態代理類。而我們通過反射獲取註解時,返回的是Java運行時生成的動態代理對象$Proxy1。通過代理對象調用自定義註解(接口)的方法,會最終調用AnnotationInvocationHandler的invoke方法。該方法會從memberValues這個Map中索引出對應的值。而memberValues的來源是Java常量池。

元註解:

java.lang.annotation提供了四種元註解,專門註解其他的註解(在自定義註解的時候,需要使用到元註解):@Documented –註解是否將包含在JavaDoc中 @Retention –什麼時候使用該註解 @Target –註解用於什麼地方 @Inherited – 是否允許子類繼承該註解

1.)@Retention– 定義該註解的生命週期

  ● RetentionPolicy.SOURCE : 在編譯階段丟棄。這些註解在編譯結束之後就不再有任何意義,所以它們不會寫入字節碼。@Override, @SuppressWarnings都屬於這類註解。
  ● RetentionPolicy.CLASS : 在類加載的時候丟棄。在字節碼文件的處理中有用。註解默認使用這種方式
  ● RetentionPolicy.RUNTIME : 始終不會丟棄,運行期也保留該註解,因此可以使用反射機制讀取該註解的信息。我們自定義的註解通常使用這種方式。

2.)Target – 表示該註解用於什麼地方。默認值爲任何元素,表示該註解用於什麼地方。可用的ElementType參數包括

  ● ElementType.CONSTRUCTOR:用於描述構造器
  ● ElementType.FIELD:成員變量、對象、屬性(包括enum實例)
  ● ElementType.LOCAL_VARIABLE:用於描述局部變量
  ● ElementType.METHOD:用於描述方法
  ● ElementType.PACKAGE:用於描述包
  ● ElementType.PARAMETER:用於描述參數
  ● ElementType.TYPE:用於描述類、接口(包括註解類型)enum聲明

3.)@Documented–一個簡單的Annotations標記註解,表示是否將註解信息添加在java文檔中。

4.)@Inherited – 定義該註釋和子類的關係 @Inherited 元註解是一個標記註解,@Inherited闡述了某個被標註的類型是被繼承的。如果一個使用了@Inherited修飾的annotation類型被用於一個class,則這個annotation將被用於該class的子類。

JDK裏的註解

JDK 內置註解 先來看幾個 Java 內置的註解,讓大家熱熱身。

@Override 演示

class Parent {
    public void run() {
    }
}

class Son extends Parent {
    /**
     * 這個註解是爲了檢查此方法是否真的是重寫父類的方法
     * 這時候就不用我們用肉眼去觀察到底是不是重寫了
     */
    @Override
    public void run() {
    }
}

還有 @Deprecated ,@SuppressWarnings , @FunctionalInterface等

註解處理器實戰

註解處理器 註解處理器纔是使用註解整個流程中最重要的一步了。所有在代碼中出現的註解,它到底起了什麼作用,都是在註解處理器中定義好的。概念:註解本身並不會對程序的編譯方式產生影響,而是註解處理器起的作用;註解處理器能夠通過在運行時使用反射獲取在程序代碼中的使用的註解信息,從而實現一些額外功能。前提是我們自定義的註解使用的是 RetentionPolicy.RUNTIME 修飾的。這也是我們在開發中使用頻率很高的一種方式。

我們先來了解下如何通過在運行時使用反射獲取在程序中的使用的註解信息。如下類註解和方法註解。
Class aClass = ApiController.class;
Annotation[] annotations = aClass.getAnnotations();

for(Annotation annotation : annotations) {
    if(annotation instanceof ApiAuthAnnotation) {
        ApiAuthAnnotation apiAuthAnnotation = (ApiAuthAnnotation) annotation;
        System.out.println("name: " + apiAuthAnnotation.name());
        System.out.println("age: " + apiAuthAnnotation.age());
    }
}
方法註解
Method method = ... //通過反射獲取方法對象
Annotation[] annotations = method.getDeclaredAnnotations();

for(Annotation annotation : annotations) {
    if(annotation instanceof ApiAuthAnnotation) {
        ApiAuthAnnotation apiAuthAnnotation = (ApiAuthAnnotation) annotation;
        System.out.println("name: " + apiAuthAnnotation.name());
        System.out.println("age: " + apiAuthAnnotation.age());
    }
}

此部分內容可參考: 通過反射獲取註解信息

註解處理器實戰 接下來我通過在公司中的一個實戰改編來演示一下註解處理器的真實使用場景。需求: 網站後臺接口只能是年齡大於 18 歲的才能訪問,否則不能訪問 前置準備: 定義註解(這裏使用上文的完整註解),使用註解(這裏使用上文中使用註解的例子) 接下來要做的事情: 寫一個切面,攔截瀏覽器訪問帶註解的接口,取出註解信息,判斷年齡來確定是否可以繼續訪問。

在 dispatcher-servlet.xml 文件中定義 aop 切面

<aop:config>
    <!--定義切點,切的是我們自定義的註解-->
    <aop:pointcut id="apiAuthAnnotation" expression="@annotation(cn.caijiajia.devops.aspect.ApiAuthAnnotation)"/>
    <!--定義切面,切點是 apiAuthAnnotation,切面類即註解處理器是 apiAuthAspect,主處理邏輯在方法名爲 auth 的方法中-->
    <aop:aspect ref="apiAuthAspect">
        <aop:around method="auth" pointcut-ref="apiAuthAnnotation"/>
    </aop:aspect>
</aop:config>

切面類處理邏輯即註解處理器代碼如

@Component("apiAuthAspect")
public class ApiAuthAspect {

    public Object auth(ProceedingJoinPoint pjp) throws Throwable {
        Method method = ((MethodSignature) pjp.getSignature()).getMethod();
        ApiAuthAnnotation apiAuthAnnotation = method.getAnnotation(ApiAuthAnnotation.class);
        Integer age = apiAuthAnnotation.age();
        if (age > 18) {
            return pjp.proceed();
        } else {
            throw new RuntimeException("你未滿18歲,禁止訪問");
        }
    }
}
註解的獲取方式
類註解

你可以在運行期訪問類,方法或者變量的註解信息,下是一個訪問類註解的例子:

Class aClass = TheClass.class;
Annotation[] annotations = aClass.getAnnotations();

for(Annotation annotation : annotations){
    if(annotation instanceof MyAnnotation){
        MyAnnotation myAnnotation = (MyAnnotation) annotation;
        System.out.println("name: " + myAnnotation.name());
        System.out.println("value: " + myAnnotation.value());
    }
}

你還可以像下面這樣指定訪問一個類的註解:

Class aClass = TheClass.class;
Annotation annotation = aClass.getAnnotation(MyAnnotation.class);

if(annotation instanceof MyAnnotation){
    MyAnnotation myAnnotation = (MyAnnotation) annotation;
    System.out.println("name: " + myAnnotation.name());
    System.out.println("value: " + myAnnotation.value());
}
方法註解

下面是一個方法註解的例子:

public class TheClass {
  @MyAnnotation(name="someName",  value = "Hello World")
  public void doSomething(){}
}

你可以像這樣訪問方法註解:

Method method = ... //獲取方法對象
Annotation[] annotations = method.getDeclaredAnnotations();

for(Annotation annotation : annotations){
    if(annotation instanceof MyAnnotation){
        MyAnnotation myAnnotation = (MyAnnotation) annotation;
        System.out.println("name: " + myAnnotation.name());
        System.out.println("value: " + myAnnotation.value());
    }
}

可以像這樣訪問指定的方法註解:

Method method = ... // 獲取方法對象
Annotation annotation = method.getAnnotation(MyAnnotation.class);

if(annotation instanceof MyAnnotation){
    MyAnnotation myAnnotation = (MyAnnotation) annotation;
    System.out.println("name: " + myAnnotation.name());
    System.out.println("value: " + myAnnotation.value());
}
參數註解

方法參數也可以添加註解,就像下面這樣:

public class TheClass {
  public static void doSomethingElse(
        @MyAnnotation(name="aName", value="aValue") String parameter){
  }
}

你可以通過 Method對象來訪問方法參數註解:

Method method = ... //獲取方法對象
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
Class[] parameterTypes = method.getParameterTypes();

int i=0;
for(Annotation[] annotations : parameterAnnotations){
  Class parameterType = parameterTypes[i++];

  for(Annotation annotation : annotations){
    if(annotation instanceof MyAnnotation){
        MyAnnotation myAnnotation = (MyAnnotation) annotation;
        System.out.println("param: " + parameterType.getName());
        System.out.println("name : " + myAnnotation.name());
        System.out.println("value: " + myAnnotation.value());
    }
  }
}

需要注意的是 Method.getParameterAnnotations()方法返回一個註解類型的二維數組,每一個方法的參數包含一個註解數組。

變量註解

下面是一個變量註解的例子:

public class TheClass {

  @MyAnnotation(name="someName",  value = "Hello World")
  public String myField = null;
}

你可以像這樣來訪問變量的註解:

Field field = ... //獲取方法對象</pre>
<pre>Annotation[] annotations = field.getDeclaredAnnotations();

for(Annotation annotation : annotations){
 if(annotation instanceof MyAnnotation){
 MyAnnotation myAnnotation = (MyAnnotation) annotation;
 System.out.println("name: " + myAnnotation.name());
 System.out.println("value: " + myAnnotation.value());
 }
}

你可以像這樣訪問指定的變量註解:

Field field = ...//獲取方法對象</pre>
Annotation annotation = field.getAnnotation(MyAnnotation.class);

if(annotation instanceof MyAnnotation){
 MyAnnotation myAnnotation = (MyAnnotation) annotation;
 System.out.println("name: " + myAnnotation.name());
 System.out.println("value: " + myAnnotation.value());
}
Java註解相關面試題
什麼是註解?他們的典型用例是什麼?

註解是綁定到程序源代碼元素的元數據,對運行代碼的操作沒有影響。

他們的典型用例是:

編譯器的信息 - 使用註解,編譯器可以檢測錯誤或抑制警告
編譯時和部署時處理 - 軟件工具可以處理註解並生成代碼,配置文件等。
運行時處理 - 可以在運行時檢查註解以自定義程序的行爲

描述標準庫中一些有用的註解。

java.lang和java.lang.annotation包中有幾個註解,更常見的包括但不限於此:

@Override -標記方法是否覆蓋超類中聲明的元素。如果它無法正確覆蓋該方法,編譯器將發出錯誤
@Deprecated - 表示該元素已棄用且不應使用。如果程序使用標有此批註的方法,類或字段,編譯器將發出警告
@SuppressWarnings - 告訴編譯器禁止特定警告。在與泛型出現之前編寫的遺留代碼接口時最常用的
@FunctionalInterface - 在Java 8中引入,表明類型聲明是一個功能接口,可以使用Lambda Expression提供其實現

可以從註解方法聲明返回哪些對象類型?

返回類型必須是基本類型,String,Class,Enum或數組類型之一。否則,編譯器將拋出錯誤。

這是一個成功遵循此原則的示例代碼:

enum Complexity {
    LOW, HIGH
}

public @interface ComplexAnnotation {
    Class<? extends Object> value();

    int[] types();

    Complexity complexity();
}

下一個示例將無法編譯,因爲Object不是有效的返回類型:

public @interface FailingAnnotation {
    Object complexity();
}
哪些程序元素可以註解?

註解可以應用於整個源代碼的多個位置。它們可以應用於類,構造函數和字段的聲明:

@SimpleAnnotation
public class Apply {
    @SimpleAnnotation
    private String aField;

    @SimpleAnnotation
    public Apply() {
        // ...
    }
}

方法及其參數:

@SimpleAnnotation
public void aMethod(@SimpleAnnotation String param) {
    // ...
}

局部變量,包括循環和資源變量:

@SimpleAnnotation
int i = 10;

for (@SimpleAnnotation int j = 0; j < i; j++) {
    // ...
}

try (@SimpleAnnotation FileWriter writer = getWriter()) {
    // ...
} catch (Exception ex) {
    // ...
}

其他註解類型:

@SimpleAnnotation
public @interface ComplexAnnotation {
    // ...
}

甚至包,通過package-info.java文件:

@PackageAnnotation
package com.baeldung.interview.annotations;
有沒有辦法限制可以應用註解的元素?

有,@ Target註解可用於此目的。如果我們嘗試在不適用的上下文中使用註解,編譯器將發出錯誤。

以下是僅將@SimpleAnnotation批註的用法限制爲字段聲明的示例:

@Target(ElementType.FIELD)
public @interface SimpleAnnotation {
    // ...
}

如果我們想讓它適用於更多的上下文,我們可以傳遞多個常量:

@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PACKAGE })

我們甚至可以製作一個註解,因此它不能用於註解任何東西。當聲明的類型僅用作複雜註解中的成員類型時,這可能會派上用場:

@Target({})
public @interface NoTargetAnnotation {
    // ...
}
什麼是元註解?

元註解適用於其他註解的註解。

所有未使用@Target標記或使用它標記但包含ANNOTATION_TYPE常量的註解也是元註解:

@Target(ElementType.ANNOTATION_TYPE)
public @interface SimpleAnnotation {
    // ...
}
下面的代碼會編譯嗎?
@Target({ ElementType.FIELD, ElementType.TYPE, ElementType.FIELD })
public @interface TestAnnotation {
    int[] value() default {};
}

不能。如果在@Target註解中多次出現相同的枚舉常量,那麼這是一個編譯時錯誤。

刪除重複常量將使代碼成功編譯:

@Target({ ElementType.FIELD, ElementType.TYPE})
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章