一文搞懂Java註解

一文搞懂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

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