夯實Java基礎系列15:Java註解簡介和最佳實踐

本系列文章將整理到我在GitHub上的《Java面試指南》倉庫,更多精彩內容請到個人倉庫裏查看html

https://github.com/h2pl/Java-Tutorial前端

喜歡的話麻煩點下Star哈java

文章首發於個人我的博客:python

www.how2playlife.comgit

本文是微信公衆號【Java技術江湖】的《夯實Java基礎系列博文》其中一篇,本文部份內容來源於網絡,爲了把本文主題講得清晰透徹,也整合了不少我認爲不錯的技術博客內容,引用其中了一些比較好的博客文章,若有侵權,請聯繫做者。程序員

該系列博文會告訴你如何從入門到進階,一步步地學習Java基礎知識,並上手進行實戰,接着瞭解每一個Java知識點背後的實現原理,更完整地瞭解整個Java技術體系,造成本身的知識框架。爲了更好地總結和檢驗你的學習成果,本系列文章也會提供部分知識點對應的面試題以及參考答案。github

若是對本系列文章有什麼建議,或者是有什麼疑問的話,也能夠關注公衆號【Java技術江湖】聯繫做者,歡迎你參與本系列博文的創做和修訂。
面試

Java註解簡介

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

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

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

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

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

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

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

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

因此,基於如此,我完成了自個人知識認知升級,我決定用標籤來解釋註解。

註解如同標籤

以前某新聞客戶端的評論有蓋樓的習慣,因而 「喬布斯從新定義了手機、羅永浩從新定義了傻X」 就常常極爲工整地出如今了評論樓層中,而且廣大網友在至關長的一段時間內對於這種行爲樂此不疲。這其實就是等同於貼標籤的行爲。
在某些網友眼中,羅永浩就成了傻X的代名詞。

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

而在網絡的另外一邊,老羅靠他的人格魅力天然收穫一大批忠實的擁泵,他們對於老羅貼的又是另外一種標籤。 

老羅仍是老羅,可是因爲人們對於它貼上的標籤不一樣,因此形成對於他的見解截然不同,不喜歡他的人成天在網絡上評論抨擊嘲諷,而崇拜欣賞他的人則會願意掙錢購買錘子手機的發佈會門票。

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

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

咱們能夠抽象歸納一下,標籤是對事物行爲的某些角度的評價與解釋。

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

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

在未開始學習任何註解具體語法而言,你能夠把註解當作一張標籤。這有助於你快速地理解它的大體做用。若是初學者在學習過程有大腦放空的時候,請不要慌張,對本身說:

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

什麼是註解?

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

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

註解的用處:

一、生成文檔。這是最多見的,也是java 最先提供的註解。經常使用的有@param @return 等
二、跟蹤代碼依賴性,實現替代配置文件功能。好比Dagger 2依賴注入,將來java開發,將大量註解配置,具備很大用處;
三、在編譯時進行格式檢查。如@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 演示
class Parent {

/**
 * 此註解表明過期了,可是若是能夠調用到,固然也能夠正常使用
 * 可是,此方法有可能在之後的版本升級中會被慢慢的淘汰
 * 能夠放在類,變量,方法上面都起做用
 */
@Deprecated
public void run() {
}
}

public class JDKAnnotationDemo {
    public static void main(String[] args) {
        Parent parent = new Parent();
        parent.run(); // 在編譯器中此方法會顯示過期標誌
    }
}

@SuppressWarnings 演示
class Parent {

// 由於定義的 name 沒有使用,那麼編譯器就會有警告,這時候使用此註解能夠屏蔽掉警告
// 即任意不想看到的編譯時期的警告均可以用此註解屏蔽掉,可是不推薦,有警告的代碼最好仍是處理一下
@SuppressWarnings("all")
private String name;
}

@FunctionalInterface 演示
/**

  • 此註解是 Java8 提出的函數式接口,接口中只容許有一個抽象方法
  • 加上這個註解以後,類中多一個抽象方法或者少一個抽象方法都會報錯
    */
    @FunctionalInterface
    interface Func {
    void run();
    }

註解處理器實戰

註解處理器
註解處理器纔是使用註解整個流程中最重要的一步了。全部在代碼中出現的註解,它到底起了什麼做用,都是在註解處理器中定義好的。
概念:註解自己並不會對程序的編譯方式產生影響,而是註解處理器起的做用;註解處理器可以經過在運行時使用反射獲取在程序代碼中的使用的註解信息,從而實現一些額外功能。前提是咱們自定義的註解使用的是 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>
<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;

從Java 8開始,它們也能夠應用於類型的使用。爲此,註解必須指定值爲ElementType.USE的@Target註解:

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

如今,註解能夠應用於類實例建立:

new @SimpleAnnotation Apply();

類型轉換:

aString = (@SimpleAnnotation String) something;

接口中:

public class SimpleList<T>
  implements @SimpleAnnotation List<@SimpleAnnotation T> {
    // ...
}

拋出異常上:

void aMethod() throws @SimpleAnnotation Exception {
    // ...
}

有沒有辦法限制能夠應用註解的元素?

有,@ 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})

參考文章

https://blog.fundodoo.com/2018/04/19/130.html

https://blog.csdn.net/qq_37939251/article/details/83215703

https://blog.51cto.com/4247649/2109129

https://www.jianshu.com/p/2f2460e6f8e7

https://blog.csdn.net/yuzongtao/article/details/83306182

微信公衆號

我的公衆號:程序員黃小斜


黃小斜是 985 碩士,阿里巴巴Java工程師,在自學編程、技術求職、Java學習等方面有豐富經驗和獨到看法,但願幫助到更多想要從事互聯網行業的程序員們。

做者專一於 JAVA 後端技術棧,熱衷於分享程序員乾貨、學習經驗、求職心得,以及自學編程和Java技術棧的相關乾貨。

黃小斜是一個斜槓青年,堅持學習和寫做,相信終身學習的力量,但願和更多的程序員交朋友,一塊兒進步和成長!

原創電子書:
關注微信公衆號【程序員黃小斜】後回覆【原創電子書】便可領取我原創的電子書《菜鳥程序員修煉手冊:從技術小白到阿里巴巴Java工程師》這份電子書總結了我2年的Java學習之路,包括學習方法、技術總結、求職經驗和麪試技巧等內容,已經幫助不少的程序員拿到了心儀的offer!

程序員3T技術學習資源: 一些程序員學習技術的資源大禮包,關注公衆號後,後臺回覆關鍵字 「資料」 便可免費無套路獲取,包括Java、python、C++、大數據、機器學習、前端、移動端等方向的技術資料。同時也包括我原創的【程序員校招大禮包】、【求職面試大禮包】等內容贈送,都是各位求職或者面試路上小夥伴很是實用的內容。

技術公衆號:Java技術江湖

若是你們想要實時關注我更新的文章以及分享的乾貨的話,能夠關注個人微信公衆號【Java技術江湖】

這是一位阿里 Java 工程師的技術小站。做者黃小斜,專一 Java 相關技術:SSM、SpringBoot、MySQL、分佈式、中間件、集羣、Linux、網絡、多線程,偶爾講點Docker、ELK,同時也分享技術乾貨和學習經驗,致力於Java全棧開發!

Java工程師必備學習資源: 關注公衆號【Java技術江湖】後回覆 Java 便可領取 Java基礎、進階、項目和架構師等免費學習資料,更有數據庫、分佈式、微服務等熱門技術學習視頻,內容豐富,兼顧原理和實踐,另外也將贈送做者原創的Java學習指南、Java程序員面試指南等乾貨資源

個人公衆號

相關文章
相關標籤/搜索