教科書級講解,秒懂最詳細Java的註解
所有知識體系文章,GitHub已收錄,歡迎Star!再次感謝,願你早日進入大廠!
GitHub地址: https://github.com/Ziphtracks/JavaLearningmanual
Java註解
一、Java註解概述
註解(Annotation),也叫元資料。一種程式碼級別的說明。它是JDK1.5及以後版本引入的一個特性,與類、介面、列舉是在同一個層次。它可以宣告在包、類、欄位、方法、區域性變數、方法引數等的前面,用來對這些元素進行說明,註釋。
二、註解的作用分類
- 編寫文件: 通過程式碼裡標識的元資料生成文件【生成文件doc文件】
- 程式碼分析: 通過程式碼裡標識的元資料對程式碼進行分析【使用反射】
- 編譯檢查: 通過程式碼裡標識的元資料讓編譯器能夠實現基本的編譯檢查【Override等】
編寫文件
首先,我們要知道Java中是有三種註釋的,分別為單行註釋、多行註釋和文件註釋。而文件註釋中,也有@開頭的元註解,這就是基於文件註釋的註解。我們可以使用javadoc命令來生成doc文件,此時我們文件的內元註解也會生成對應的文件內容。這就是編寫文件的作用。
程式碼分析
我們頻繁使用之一,也是包括使用反射來通過程式碼裡標識的元資料對程式碼進行分析的,此內容我們在後續展開講解。
編譯檢查
至於在編譯期間在程式碼中標識的註解,可以用來做特定的編譯檢查,它可以在編譯期間就檢查出“你是否按規定辦事”,如果不按照註解規定辦事的話,就會在編譯期間飄紅報錯,並予以提示資訊。可以就可以為我們程式碼提供了一種規範制約,避免我們後續在程式碼中處理太多的程式碼以及功能的規範。比如,@Override註解是在我們覆蓋父類(父介面)方法時出現的,這證明我們覆蓋方法是繼承於父類(父介面)的方法,如果該方法稍加改變就會報錯;@FunctionInterface註解是在編譯期檢查是否是函式式介面的,如果不遵循它的規範,同樣也會報錯。
三、jdk的內建註解
3.1 內建註解分類
- @Override: 標記在成員方法上,用於標識當前方法是重寫父類(父介面)方法,編譯器在對該方法進行編譯時會檢查是否符合重寫規則,如果不符合,編譯報錯。
- @Deprecated: 用於標記當前類、成員變數、成員方法或者構造方法過時如果開發者呼叫了被標記為過時的方法,編譯器在編譯期進行警告。
- @SuppressWarnings: 壓制警告註解,可放置在類和方法上,該註解的作用是阻止編譯器發出某些警告資訊。
3.2 @Override註解
標記在成員方法上,用於標識當前方法是重寫父類(父介面)方法,編譯器在對該方法進行編譯時會檢查是否符合重寫規則,如果不符合,編譯報錯。
這裡解釋一下@Override註解,在我們的Object基類中有一個方法是toString方法,我們通常在實體類中去重寫此方法來達到列印物件資訊的效果,這時候也會發現重寫的toString方法上方就有一個@Override註解。如下所示:
於是,我們試圖去改變重寫後的toString方法名稱,將方法名改為toStrings。你會發現在編譯期就報錯了!如下所示:
那麼這說明什麼呢?這就說明該方法不是我們重寫其父類(Object)的方法。這就是@Override註解的作用。
3.3 @Deprecated註解
用於標記當前類、成員變數、成員方法或者構造方法過時如果開發者呼叫了被標記為過時的方法,編譯器在編譯期進行警告。
我們解釋@Deprecated註解就需要模擬一種場景了。假設我們公司的產品,目前是V1.0版本,它為使用者提供了show1方法的功能。這時候我們為產品的show1方法的功能又進行了擴充套件,打算髮布V2.0版本。但是,我們V1.0版本的產品需要拋棄嗎?也就是說我們V1.0的產品功能還繼續讓使用者使用嗎?答案肯定是不能拋棄的,因為有一部分使用者是一直用V1.0版本的。如果拋棄了該版本會損失很多的使用者量,所以我們不能拋棄該版本。這時候,我們對功能進行了擴充套件後,釋出了V2.0版本,我們給予使用者的通知就可以了,也就是告知使用者我們在V2.0版本中為功能進行了擴充套件。可以讓使用者自行選擇版本。
但是,除了釋出告知使用者版本情況之外,我們還需要在原來版本的功能上給予提示,在上面的模擬場景中我們需要在show1方法上方加@Deprecated註解給予提示。通過這種方式也告知使用者“這是舊版本時候的功能了,我們不建議再繼續使用舊版本的功能”,這句話的意思也就正是給使用者做了提示。使用者也會這麼想“奧,這版本的這個功能不好用了,肯定有新版本,又更好用的功能。我要去官網查一下下載新版本”,還會有使用者這麼想“我明白了,又更新出更好的功能了,但是這個版本的功能我已經夠用了,不需要重新下載新版本了”。
那麼我們怎麼檢視我上述所說的在功能上給予的提示呢?這時候我需要去建立一個方法,然後去呼叫show1方法,並檢視呼叫時它是如何提示的。
圖已經貼出來了,你是否發現的新舊版本功能的異同點呢?很明顯,在方法中的提示是在呼叫的方法名上加了一道橫線把該方法劃掉了。這就體現了show1方法過時了,已經不建議使用了,我們為你提供了更好的。
回想起來,在我們的api中也會有方法是過時的,比如我們的Date日期類中的方法有很多都已經過時了。如下圖:
如你所見,是不是有很多方法都過時了呢?那它的方法上是加了@Deprecated註解嗎?來跟著我的腳步,我帶你們看一下。
我們已經知道的Date類中的這些方法已經是過時的了,如果我們使用該方法並執行該程式的話。執行的過程中就會提示該方法已過時的內容,但是隻是提示,並不影響你使用該方法。如下:
OK!這也就是@Deprecated註解的作用了。
3.4 @SuppressWarnings註解
壓制警告註解,可放置在類和方法上,該註解的作用是阻止編譯器發出某些警告資訊,該註解為單值註解,只有 一個value引數,該引數為字串陣列型別,引數值常用的有如下幾個。
- unchecked:未檢查的轉化,如集合沒有指定型別還新增元素
- unused:未使用的變數
- resource:有泛型未指定型別
- path:在類路徑,原檔案路徑中有不存在的路徑
- deprecation:使用了某些不贊成使用的類和方法
- fallthrough:switch語句執行到底沒有break關鍵字
- rawtypes:沒有寫泛型,比如: List list = new ArrayList();
- all:全部型別的警告
壓制警告註解,顧名思義就是壓制警告的出現。我們都知道,在Java程式碼的編寫過程中,是有很多黃色警告出現的。但是我不知道你的導師是否教過你,程式設計師只需要處理紅色的error,不需要理會黃色的warning。如果你的導師說過此問題,那是有原因的。因為在你學習階段,我們認清處理紅色的error即可,這樣可以減輕你學習階段在腦部的記憶內容。如果你剛剛加入學習Java的佇列中,需要大腦記憶的東西就有太多了,也就是我們目前不需要額外記憶其他的東西,只記憶重點即可。至於黃色warning嘛,在你的學習過程中慢慢就會有所瞭解的,而不是死記硬背的。
那為了解釋@SuppressWarnings註解,我們還使用上一個例子,因為在那個例子中就有黃色的warning出現。
而每一個黃色的warning都會有警告資訊的。比如,這一個圖中的警告資訊,就告知你show2()方法沒有被使用,簡單來說,你建立的show2方法,但是你在程式碼中並沒有呼叫過此方法。以後你便會遇到各種各樣黃色的warning。然後, 我們就可以使用不同的註解引數來壓制不同的註解。但是在該註解的引數中,提供了一個all引數可以壓制全部型別的警告。而這個註解是需要加到類的上方,並賦予all引數,即可壓制所有警告。如下:
我們加入註解並賦予all引數後,你會發現use方法和show2方法的警告沒有了,實際上導Date包的警告還在,因為我們Date包匯入到了該類中,但是我們並沒有建立Date物件,也就是並沒有寫入Date在程式碼中,你也會發現那一行是灰色的,也就證明了我們沒有去使用匯入這個包的任何資訊的說法,出現這種情況我們就需要把這個沒有用的導包內容刪除掉,使用Ctrl + X
刪除匯入沒有用到的包即可。還有一種辦法就是在包的上方修飾壓制警告註解,但是我認為在一個沒有用的包上加壓制註解是毫無意義的,所以,我們直接刪除就好。
然後,我們還見到上圖,註解那一行出現了警告資訊提示。這一行的意思是冗餘的警告壓制。這就是說我們壓制以下的警告並沒有什麼意義而造成的冗餘,但是如果我們使用了該類並做了點什麼的話,壓制註解的冗餘警告就會消失,畢竟我們使用了該類,此時就不會早場冗餘了。
上述解釋@SuppressWarnings註解也差不多就這些了。OK,繼續向下看吧。持續為大家講解。
3.5 @Repeatable註解
@Repeatable 表明標記的註解可以多次應用於相同的宣告或型別,此註解由Java8版本引入。我們知道註解是不能重複定義的,其實該註解就是一個語法糖,它可以重複多此使用,更適用於我們的特殊場景。
首先,我們先建立一個可以重複使用的註解。
package com.mylifes1110.anno;
import java.lang.annotation.Repeatable;
@Repeatable(Hour.class)
public @interface Hours {
double[] hours() default 0;
}
你會發現註解要求傳入的值是一個類物件,此類物件就需要傳入另外一個註解,這裡也就是另外一個註解容器的類物件。我們去建立一下。
package com.mylifes1110.anno;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
//容器
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Hour {
Hours[] value();
}
其實,這兩個註解的套用,就是將一個普通的註解封裝了一個可重複使用的註解,來達到註解的複用性。最後,我們建立一下測試類,隨後帶你去看一下原始碼。
package com.mylifes1110.java;
import com.mylifes1110.anno.Hours;
@Hours(hours = 4)
@Hours(hours = 4.5)
@Hours(hours = 2)
public class Worker {
public static void main(String[] args) {
//通過Hours註解型別來獲取Worker中的值陣列物件
Hours[] hours = Worker.class.getAnnotationsByType(Hours.class);
//遍歷陣列
for (Hours h : hours) {
System.out.println(h);
}
}
}
測試類,是一個工人測試類,該工人使用註解記錄早中晚的工作時間。測試結果如下:
然後我們進入到原始碼一探究竟。
我們發現進入到原始碼後,就只看見一個返回值為類物件的抽象方法。這也就驗證了該註解只是一個可實現重複性註解的語法糖而已。
四、註解分類
4.1 註解分類
註解可以根據註解引數分為三大類:
- 標記註解: 沒有引數的註解,僅用自身的存在與否為程式提供資訊,如@Override註解,該註解沒有引數,用於表示當前方法為重寫方法。
- 單值註解: 只有一個引數的註解,如果該引數的名字為value,那麼可以省略引數名,如 @SuppressWarnings(value = "all"),可以簡寫為@SuppressWarnings("all")。
- 完整註解: 有多個引數的註解。
4.2 標記註解
說到@Override註解是一個標記註解,那我們進入到該註解的原始碼檢視一下。從上往下看該註解原始碼,發現它繼承了匯入了
java.lang.annotation.*
,也就是有使用到該包的內容。然後下面就又是兩個看不懂的註解,其實發現註解的定義格式是public修飾的@Interface,最終看到該註解中方法體並沒有任何引數,也就是隻起到標記作用。
4.3 單值註解
在上面我們用到的@SuppressWarnings註解就是一個單值註解。那我們進入到它的原始碼看一下是怎麼個情況。其實,和標記註解比較,它就多一個value引數而已,而這就是單值註解的必要條件,即只有一個引數。並且這一個引數為value時,我們可以省略value。
4.4 完整註解
上述兩個型別註解講解完,至於完整註解嘛,這下就能更明白了。其中的方法體就是有多個引數而已。
五、自定義註解
5.1 自定義註解格式
格式: public @Interface 註解名 {屬性列表/無屬性}
注意: 如果註解體中無任何屬性,其本質就是標記註解。但是與其標註註解還少了上邊修飾的元註解。
如下,這就是一個註解。但是它與jdk自定義註解有點區別,jdk自定義註解的上方還有註解來修飾該註解,而那註解就叫做元註解。元註解我會在後面詳細的說到。
這裡我們的確不知道@Interface是什麼,那我們就把自定義的這個註解反編譯一下,看一下反編譯資訊。反編譯操作如下:
反編譯後的反編譯內容如下:
public interface com.mylifes1110.anno.MyAnno extends java.lang.annotation.Annotation {
}
首先,看過反編譯內容後,我們可以直觀的得知他是一個介面,因為它的public修飾符後面的關鍵字是interface。
其次,我們發現MyAnno
這個介面是繼承了java.lang.annotation
包下的Annotation
介面。
所以,我們可以得知註解的本質就是一個介面,該介面預設繼承了Annotation
介面。
既然,是繼承的Annotation
介面,那我們就去進入到這個介面中,看它定義了什麼。以下是我抽取出來的介面內容。我們發現它看似很常見,其實它們不是很常用,作為了解即可。
public interface Annotation {
boolean equals(Object obj);
int hashCode();
String toString();
Class<? extends Annotation> annotationType();
}
最後,我們的註解中也是可以寫有屬性的,它的屬性不同於普通的屬性,它的屬性是抽象方法。既然註解也是一個介面,那麼我們可以說介面體中可以定義什麼,它同樣也可以定義,而它的修飾符與介面一樣,也是預設被public abstract
修飾。
而註解體中的屬性也是有要求的。其屬性要求如下:
- 屬性的返回值型別必須是以下幾種:
- 基本資料型別
- String型別
- 列舉型別
- 註解
- 以上型別的陣列
- 注意: 在這裡不能有void的無返回值型別和以上型別以外的型別
- 定義的屬性,在使用時需要給註解中的屬性賦值
- 如果定義屬性時,使用
default
關鍵字給屬性預設初始化值,則使用註解時可以不為屬性賦值,它取的是預設值。如果為它再次傳入值,那麼就發生了對原值的覆蓋。- 如果只有一個屬性需要賦值,並且屬性的名稱為value,則賦值時value可以省略,可以直接定義值
- 陣列賦值時,值使用
{}
儲存值。如果陣列中只有一個值,則可以省略{}
。
5.2 自定義註解屬性的返回值
屬性返回值既然有以上幾種,那麼我就在這裡寫出這幾種演示一下是如何寫的。
首先,定義一個列舉類和另外一個註解備用。
package com.mylifes1110.enums;
public enum Lamp {
RED, GREEN, YELLOW
}
package com.mylifes1110.anno;
public @interface MyAnno2 {
}
其次,我們來定義上述幾種型別,如下:
package com.mylifes1110.anno;
import com.mylifes1110.enums.Lamp;
public @interface MyAnno {
//基本資料型別
int num();
//String型別
String value();
//列舉型別
Lamp lamp();
//註解型別
MyAnno2 myAnno2();
//以上型別的陣列
String[] values();
Lamp[] lamps();
MyAnno2[] myAnno2s();
int[] nums();
}
5.3 自定義註解的屬性賦值
這裡我們演示一下,首先,我們使用該註解來進行演示。
package com.mylifes1110.anno;
public @interface MyAnno {
//基本資料型別
int num();
//String型別
String value();
}
隨後建立一個測試類,在類的上方寫上註解,你會發現,註解的引數中會讓你寫這兩個引數(int、String)。
此時,傳參是這樣來做的。格式為:名稱 = 返回值型別引數
。如下:
上述所說,如果使用default關鍵字給屬性預設初始化值,就不需要為其引數賦值,如果賦值的話,就把預設初始化的值覆蓋掉了。
當然還有一個規則,如果只有一個屬性需要賦值,並且屬性的名稱為value,則賦值時value可以省略,可以直接定義值。那麼,我們的num已經有了預設值,就可以不為它傳值。我們發現,註解中定義的屬性就剩下了一個value屬性值,那麼我們就可以來演示這個規則了。
這裡,我並沒有寫屬性名稱value,而是直接為value賦值。如果我將num的default關鍵字修飾去掉呢,那意思也就是說在使用該註解時必須為num賦值,這樣可以省略value嗎?那我們看一下。
結果,就是我們所想的,它報錯了,必須讓我們給num賦值。其實想想這個規則也是很容易懂的,定義一個為value的值,就可以省略其value名稱。如果定義多個值,它們可以省略名稱就無法區分定義的是那個值了,關鍵是還有陣列,陣列內定義的是多個值呢,對吧。
5.4 自定義註解的多種返回值型別賦值
這裡我們演示一下,上述的多種返回值型別是如何賦值的。這裡我們定義這幾個引數來看一下,是如何為屬性賦值的。
num是一個int基本資料型別,即num = 1
value是一個String型別,即value = "str"
lamp是一個列舉型別,即lamp = Lamp.RED
myAnno2是一個註解型別,即myAnno2 = @MyAnno2
values是一個String型別陣列,即values = {"s1", "s2", "s3"}
values是一個String型別陣列,其陣列中只有一個值,即values = "s4"
注意: 值與值之間是,
隔開的;陣列是用{}
來儲存值的,如果陣列中只有一個值可以省略{}
;列舉型別是列舉名.列舉值
六、元註解
6.1 元註解分類
元註解就是用來描述註解的註解。一般使用元註解來限制自定義註解的使用範圍、生命週期等等。
而在jdk的中java.lang.annotation包中定義了四個元註解,如下:
元註解 | 描述 |
---|---|
@Target | 指定被修飾的註解的作用範圍 |
@Retention | 指定了被修飾的註解的生命週期 |
@Documented | 指定了被修飾的註解是可以Javadoc等工具文件化 |
@Inherited | 指定了被修飾的註解修飾程式元素的時候是可以被子類繼承的 |
6.2 @Target
@Target 指定被修飾的註解的作用範圍。其作用範圍可以在原始碼中找到引數值。
屬性 | 描述 |
---|