一文搞懂Java註解
1.概述
Java 註解用於爲 Java 代碼提供元數據。作爲元數據,註解不直接影響你的代碼執行,但也有一些類型的註解實際上可以用於這一目的。Java 註解是從 Java5 開始添加到 Java 的,用於對代碼進行說明,可以對包、類、接口、字段、方法參數、局部變量等進行註解。
官方的解釋總是讓人一臉懵逼,但有兩個點需要關注,一是元數據,二是不直接影響你的代碼執行。
元數據:是用來描述數據的數據,這裏就是指描述代碼的數據。納尼這不就是註釋麼?從一定意義上說確實和註釋一樣。
不直接影響你的代碼執行:意思是有可能可以影響代碼執行。註釋如何影響代碼執行?註解和註釋最大的不同是我們可以通過代碼運行或編譯時拿到註解,進而影響代碼執行。
Java註解不僅描述了源代碼還能間接影響代碼運行。
2.相關概念
Java中的註解主要分爲三類:
2.1 Java內置註解
Java 內置常用註解共有5個,在java.lang包中。
- @Override - 檢查該方法是否是重載方法。如果發現其父類或引用的接口中並沒有該方法時,會報編譯錯誤。
- @Deprecated - 標記過時方法。如果使用該方法,會報編譯警告。
- @SuppressWarnings - 指示編譯器去忽略註解中聲明的警告。
- @SafeVarargs - Java 7 開始支持,忽略任何使用參數爲泛型變量的方法或構造函數調用產生的警告。
- @FunctionalInterface - Java 8 開始支持,編譯器javac檢查一個接口是否符合函數接口的標準。
2.2 元註解
元註解是用於定義註解的註解。Java中的元註解都在java.lang.annotation包中,前4個是元註解,Java1.8添加了後面2個註解。
- @Retention - 標識這個註解怎麼保存,是隻在代碼中,還是編入class文件中,或者是在運行時可以通過反射訪問。
- @Documented - 標記這些註解是否包含在用戶文檔中。
- @Target - 標記這個註解應該是哪種 Java 成員。
- @Inherited - 標記這個註解是繼承於哪個註解類(默認 註解並沒有繼承於任何子類)
- @Repeatable - Java 8 開始支持,標識某註解可以在同一個聲明上使用多次。
- @Native - 標記屬性爲native屬性,用在代碼中,給IDE工具做提示使用。
2.3 自定義註解
根據自己需求使用元註解定義我們的註解。
3.使用註解
先看一個註解的定義,使用@interface
關鍵字定義一個註解,還需要使用@Retention
標記該註解保留到什麼時期,@Target
標記該註解可用在什麼地方。
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.METHOD)
public @interface Override {}
要理解註解的使用,需要從元註解入手,其中@Retention 和 @Target 是定義一個註解所必須的。
3.1 理解元註解
@Retention
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
// 返回該註解保留到什麼階段
RetentionPolicy value();
}
@Retention 的定義多了一個value()
方法,返回的是一個RetentionPolicy類型的值,這是一個枚舉類型的值說明只能有一個保留策略。這個返回值是在使用註解@Retention(RetentionPolicy.RUNTIME)
括號內的值RetentionPolicy.RUNTIME 。
public enum RetentionPolicy {
SOURCE,/*僅在源碼中保留,編譯時會刪除該註解*/
CLASS,/*保留到編譯期,在Class字節碼文件中保留,但JVM不會加載該註解;這是默認值*/
RUNTIME/*保留到運行期,JVM會加載該註解,所以可以被反射讀取*/
}
RetentionPolicy有三個值,分別標記該註解是保留在源碼、編譯期還是運行期,只有運行期的註解纔會被反射讀取。
@Target
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
// 返回該註解可以在何處使用,可以有多個位置
ElementType[] value();
}
@Target
標記該註解可以用在什麼位置,返回的是一個ElementTypep類型數組,說明一個註解可以被用在多種位置。
public enum ElementType {
/** 可用在類、接口、枚舉聲明前 */
TYPE,
/** 字段聲明(包括枚舉常量) */
FIELD,
/** 方法聲明 */
METHOD,
/** 參數聲明 */
PARAMETER,
/** 構造方法聲明 */
CONSTRUCTOR,
/** 局部變量聲明 */
LOCAL_VARIABLE,
/** 註解聲明*/
ANNOTATION_TYPE,
/** 包聲明 */
PACKAGE,
/**
* 該註解可用於類型變量的聲明語句中,如泛型聲明
* @since 1.8
*/
TYPE_PARAMETER,
/**
* 該註解能寫在使用類型的任何語句中
* @since 1.8
*/
TYPE_USE
}
TYPE_PARAMETER 和 TYPE_USE 我們會重點做示例講解,這裏先不展開說。
@Documented
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Documented {}
@Documented是一個純粹的語義元註解,生成JavaDoc文檔時,被@Documented標記的註解在所使用的地方會被文檔收錄進去,否則Java文檔不會有該註解信息。
@Inherited
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Inherited {}
@Inherited標記的註解是可以被集成的,當父類被可繼承的註解標記,子類會自動擁有該註解。在運行時將不斷向上尋找對應註解是否存在。注意在方法和接口上的註解不具有繼承特性。
@Repeatable
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Repeatable {
Class<? extends Annotation> value();
}
@Repeatable 標記的註解,可以在同一個位置存在多個
3.2 定義註解
瞭解了元註解以及使用方式後,自定義註解就很簡單了。
@元註解1
@元註解2
修飾符 @interface 註解名 {
註解元素的聲明1
註解元素的聲明2
}
(1)定義註解使用@interface
關鍵字
(2)使用@Retention
和@Target
元註解指定保留時期和使用位置。
(3)註解可以添加成員變量,定義類似方法,模式是:變量類型 變量名()
,還可以爲成員變量添加默認值,模式是:變量類型 變量名() default 默認值
。
(4)成員變量可支持基本類型、String、Class、enum、Annotation、及其以上類型的數組。
(5)沒有成員變量的註解稱爲標記,有成員變量的註解稱爲元數據。
下面舉幾個例子:
- 可用在類、接口、枚舉之前使用。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ClassAnnotaion {
String calssId() default "xbox";
int classCode();
}
- 可用在Method上的註解
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(MethodAnnotationArray.class)
public @interface MethodAnnotation {
String tag() default "方法默認tag";
}
// 可重複註解的容器
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MethodAnnotationArray {
MethodAnnotation[] value();
}
- 可用在Field和參數上的註解
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD,ElementType.PARAMETER})
public @interface FieldAnnotation {
boolean isCheck() default false;
}
- 可用在參數上的註解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface ParameterAnnotation {
String value();
}
3.3 使用註解
@ClassAnnotaion(calssIds = "bacd",classCode = 34)
public class Demo1 {
@FieldAnnotation(isCheck = true)
String name;
@MethodAnnotation(tag = "demo方法")
@MethodAnnotation(tag = "可重複註解")
public void deal(@ParameterAnnotation("kk") String input){
System.out.println(input);
}
}
要使註解只需要在對應位置添加定義的註解
@註解名(變量1 = 值1 ,變量2 = 值2, 變量3 = {數組元素1,數組元素2,數組元素3})
注意:當定義註解只有一個成員變量,且屬性名稱爲value,此時使用註解時可以在括號內直接對value賦值,而不用顯式指定value = 值。
此時在代碼上已經加上了自己定義的註解,運行這些代碼,此時的註解和普通的註釋一樣,不會影響目前的代碼。
如果想要利用這些註解達到間接改變代碼執行的目的,需要編寫專門的註解處理器,來解析並處理註解。
3.4 處理註解
註解編譯後同樣會生成ClassAnnotaion.class 字節碼,這裏使用jad 工具反編譯生成的字節碼。
// jad ClassAnnotation.class 反編譯後代碼如下:
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name: ClassAnnotaion.java
package com.company.part1;
import java.lang.annotation.Annotation;
public interface ClassAnnotaion extends Annotation{
public abstract String calssIds();
public abstract int classCode();
}
從中可以看出,註解其實就是一個接口,並且繼承了Annotation接口。該接口在java.lang.annotation
包。
public interface Annotation {
boolean equals(Object obj);
int hashCode();
String toString();
/**
* @return the annotation type of this annotation
*/
Class<? extends java.lang.annotation.Annotation> annotationType();
}
通過以上源碼可以看出,註解本質上就是一個接口,接口可以有成員變量和成員方法,但接口中的成員變量時static final 類型的,使用時無法重新賦值,這對註解來說是沒意義的;所以註解使用成員方法來當做註解成員變量,這也解釋了定義註解時,成員變量爲什麼帶有括號。
要處理註解需要使用反射,註解可以在很多地方使用,java在java.lang.reflect
包中定義了一個AnnotatedElement
接口,這個接口定義了可以使用註解的類型,簡而言之實現了該接口的類型都可以使用註解。
public interface AnnotatedElement {
// 指定類型的註解是否在當前元素上
default boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {
return getAnnotation(annotationClass) != null;
}
// 在當前元素上獲取指定類型註解的實例,有則返回,無則返回null
<T extends Annotation> T getAnnotation(Class<T> annotationClass);
// 返回當前元素上所有的註解,包括當前元素繼承父類的註解。
Annotation[] getAnnotations();
// 返回當前元素上直接存在的註解。
Annotation[] getDeclaredAnnotations();
//------------------以下1.8新增-------------------
// 獲取當前元素上指定類型的重複註解實例,
default <T extends Annotation> T[] getAnnotationsByType(Class<T> annotationClass) {
T[] result = getDeclaredAnnotationsByType(annotationClass);
if (result.length == 0 && // Neither directly nor indirectly present
this instanceof Class && // the element is a class
AnnotationType.getInstance(annotationClass).isInherited()) { // Inheritable
Class<?> superClass = ((Class<?>) this).getSuperclass();
if (superClass != null) {
// Determine if the annotation is associated with the
// superclass
result = superClass.getAnnotationsByType(annotationClass);
}
}
return result;
}
// 獲取當前元素上直接存在的指定類型註解
default <T extends Annotation> T getDeclaredAnnotation(Class<T> annotationClass) {
Objects.requireNonNull(annotationClass);
// Loop over all directly-present annotations looking for a matching one
for (Annotation annotation : getDeclaredAnnotations()) {
if (annotationClass.equals(annotation.annotationType())) {
// More robust to do a dynamic cast at runtime instead
// of compile-time only.
return annotationClass.cast(annotation);
}
}
return null;
}
// 獲取當前元素上指定類型的重複註解
default <T extends Annotation> T[] getDeclaredAnnotationsByType(Class<T> annotationClass) {
Objects.requireNonNull(annotationClass);
return AnnotationSupport.
getDirectlyAndIndirectlyPresent(Arrays.stream(getDeclaredAnnotations()).
collect(Collectors.toMap(Annotation::annotationType,
Function.identity(),
((first,second) -> first),
LinkedHashMap::new)),
annotationClass);
}
}
在1.8之前,一個元素上只能有一個相同類型的註解,1.8之後被Repeatable標記的註解在使用時可在一個元素上同時標註多個。
上面講元註解**@Target**時提到了ElementType枚舉,可以用於定義註解可以使用在哪些地方,下面列舉部分。
ElementType | 實現類型 | 含義 |
---|---|---|
TYPE | Class | 代表了類、接口、枚舉 |
FIELD | Field | 代表了成員變量 |
METHOD | Method | 代表了成員方法 |
CONSTRUCTOR | Constructor | 代表了構造函數 |
PARAMETER | Parameter | 代表了參數 |
… |
Class 定義如下,其實現了AnnotatedElement接口。
public final class Class<T> implements java.io.Serializable,
GenericDeclaration,
Type,
AnnotatedElement {}
Field 定義如下,同樣實現該接口:
class Field extends AccessibleObject implements Member {}
Constructor和Method定義如下:
public class AccessibleObject implements AnnotatedElement {}
public abstract class Executable extends AccessibleObject implements Member, GenericDeclaration {}
public final class Constructor<T> extends Executable {}
public final class Method extends Executable {]
Parameter定義如下:
public final class Parameter implements AnnotatedElement {}
所以通過反射來處理註解可以簡單分爲兩步:
第一步:通過反射拿到想要處理的元素上的註解。
第二步:拿到註解中屬性值,做進一步處理。
(1)處理類上的註解
Demo1 demo1 = new Demo1();
clazz = demo1.getClass();
if (clazz.isAnnotationPresent(ClassAnnotaion.class)) {
ClassAnnotaion classAnnotaion = (ClassAnnotaion)
clazz.getDeclaredAnnotation(ClassAnnotaion.class);
System.out.println("類型註解值:" +
classAnnotaion.calssIds() + " " +
classAnnotaion.classCode()
);
}
(2)處理屬性上的註解
Field field = clazz.getField("name");
if (field.isAnnotationPresent(FieldAnnotation.class)) {
FieldAnnotation fieldAnnotation = field.getDeclaredAnnotation(FieldAnnotation.class);
System.out.println("屬性註解值:" + fieldAnnotation.isCheck());
}
(3)處理方法上的註解
Method method = clazz.getMethod("deal", String.class, String.class);
if (method.isAnnotationPresent(MethodAnnotationArray.class)) {
MethodAnnotationArray methodAnnotationArray =
method.getDeclaredAnnotation(MethodAnnotationArray.class);
MethodAnnotation[] methodAnnotations = methodAnnotationArray.value();
for (MethodAnnotation temp : methodAnnotations) {
System.out.println("方法註解值:" + temp.tag());
}
}
可重複註解需要指定一個容器,雖然寫的時候使用的是MethodAnnotation類型,但最終編譯後會被裝進MethodAnnotationArray,最終在方法上合併成一個MethodAnnotationArray類型註解。
(4)處理參數註解
Annotation[][] annotations1 = method.getParameterAnnotations();
for (int i = 0; i < annotations1.length; i++) {
System.out.println("第" + (i + 1) + "個參數的註解");
Annotation[] tmp = annotations1[i];
for (Annotation a : tmp) {
System.out.println(tmp);
}
}
一個方法有多個參數,一個方法又有多個參數,所以這裏使用一個二維數組來存儲參數列表中的註解。
4.自定義註解應用
自定義註解在工作中的應用還是很多的,這塊準備自己再積累積累再做補充,使用+原理。
1.生成文檔.例如:@see,@param,@return 等
2.代替配置文件功能.例如spring基於註解的配置
3.在編譯時進行格式檢查。
4.JUnit 、ButterKnife、Dagger2、Retrofit
參考:
Java 註解完全解析,by 若丨寒
深入理解java註解的實現原理, by 知了123
java元註解 @Target註解用法, by 就這個名字好
Java反射API研究(1)——註解Annotation, by 光閃
Java枚舉和註解梳理, by itzhouq的博客
Java之註解的定義及使用, by 鍊金術師cck