【廖雪峯官方網站/Java教程】註解

本節我們將介紹Java程序的一種特殊“註釋”——註解(Annotation)。

1.使用註解

1.1.註解入門示例

什麼是註解(Annotation)?註解是放在Java源碼的類、方法、字段、參數前的一種特殊“註釋”,如下例子:

// this is a component:
@Resource("hello")
public class Hello {
    @Inject
    int n;

    @PostConstruct
    public void hello(@Param String name) {
        System.out.println(name);
    }

    @Override
    public String toString() {
        return "Hello";
    }
}

註釋會被編譯器直接忽略,註解則可以被編譯器打包進入class文件,因此,註解是一種用作標註的“元數據”。

1.2.註解的作用

從JVM的角度看,註解本身對代碼邏輯沒有任何影響,如何使用註解完全由工具決定。

1.2.1.註解分類

Java的註解可以分爲三類:

  • 第一類是由編譯器使用的註解,例如:
    @Override:讓編譯器檢查該方法是否正確地實現了覆寫;
    @SuppressWarnings:告訴編譯器忽略此處代碼產生的警告。
    這類註解不會被編譯進入.class文件,它們在編譯後就被編譯器扔掉了

  • 第二類是由工具處理.class文件使用的註解,比如有些工具會在加載class的時候,對class做動態修改,實現一些特殊的功能。這類註解會被編譯進入.class文件,但加載結束後並不會存在於內存中。這類註解只被一些底層庫使用,一般我們不必自己處理。

  • 第三類是在程序運行期能夠讀取的註解,它們在加載後一直存在於JVM中,這也是最常用的註解。例如,一個配置了@PostConstruct的方法會在調用構造方法後自動被調用(這是Java代碼讀取該註解實現的功能,JVM並不會識別該註解)。

1.2.2.註解參數配置

定義一個註解時,還可以定義配置參數。配置參數可以包括:

  • 所有基本類型;
  • String;
  • 枚舉類型;
  • 基本類型、String以及枚舉的數組。

因爲配置參數必須是常量,所以,上述限制保證了註解在定義時就已經確定了每個參數的值。
註解的配置參數可以有默認值,缺少某個配置參數時將使用默認值。此外,大部分註解會有一個名爲value的配置參數,對此參數賦值,可以只寫常量,相當於省略了value參數。如果只寫註解,相當於全部使用默認值。
舉個栗子,對以下代碼:

public class Hello {
    @Check(min=0, max=100, value=55)
    public int n;

    @Check(value=99)
    public int p;

    @Check(99) // @Check(value=99)
    public int x;

    @Check
    public int y;
}

@Check就是一個註解。第一個@Check(min=0, max=100, value=55)明確定義了三個參數,第二個@Check(value=99)只定義了一個value參數,它實際上和@Check(99)是完全一樣的。最後一個@Check表示所有參數都使用默認值。

2.定義註解

Java語言使用@interface語法來定義註解(Annotation),它的格式如下:

public @interface Report {
    int type() default 0;
    String level() default "info";
    String value() default "";
}

註解的參數類似無參數方法,可以用default設定一個默認值(強烈推薦)。最常用的參數應當命名爲value。

2.1.元註解

有一些註解可以修飾其他註解,這些註解就稱爲元註解(meta annotation)。Java標準庫已經定義了一些元註解,我們只需要使用元註解,通常不需要自己去編寫元註解。

2.1.1.@Target

最常用的元註解是@Target。使用@Target可以定義Annotation能夠被應用於源碼的哪些位置:

  • 類或接口:ElementType.TYPE;
  • 字段:ElementType.FIELD;
  • 方法:ElementType.METHOD;
  • 構造方法:ElementType.CONSTRUCTOR;
  • 方法參數:ElementType.PARAMETER。

例如,定義註解@Report可用在方法上,我們必須添加一個@Target(ElementType.METHOD):

@Target(ElementType.METHOD)
public @interface Report {
    int type() default 0;
    String level() default "info";
    String value() default "";
}

定義註解@Report可用在方法或字段上,可以把@Target註解參數變爲數組{ ElementType.METHOD, ElementType.FIELD }:

@Target({
    ElementType.METHOD,
    ElementType.FIELD
})
public @interface Report {
    ...
}

實際上@Target定義的value是ElementType[]數組,只有一個元素時,可以省略數組的寫法。

2.1.2.@Retention

另一個重要的元註解@Retention定義了Annotation的生命週期:

  • 僅編譯期:RetentionPolicy.SOURCE;
  • 僅class文件:RetentionPolicy.CLASS;
  • 運行期:RetentionPolicy.RUNTIME。

如果@Retention不存在,則該Annotation默認爲CLASS。因爲通常我們自定義的Annotation都是RUNTIME,所以,務必要加上@Retention(RetentionPolicy.RUNTIME)這個元註解:

@Retention(RetentionPolicy.RUNTIME)
public @interface Report {
    int type() default 0;
    String level() default "info";
    String value() default "";
}

2.1.3.@Repeatable

使用@Repeatable這個元註解可以定義Annotation是否可重複。這個註解應用不是特別廣泛。

@Repeatable(Reports.class)
@Target(ElementType.TYPE)
public @interface Report {
    int type() default 0;
    String level() default "info";
    String value() default "";
}

@Target(ElementType.TYPE)
public @interface Reports {
    Report[] value();
}

經過@Repeatable修飾後,在某個類型聲明處,就可以添加多個@Report註解:

@Report(type=1, level="debug")
@Report(type=2, level="warning")
public class Hello {
}

2.1.4.@Inherited

使用@Inherited定義子類是否可繼承父類定義的Annotation。@Inherited僅針對@Target(ElementType.TYPE)類型的annotation有效,並且僅針對class的繼承,對interface的繼承無效:

@Inherited
@Target(ElementType.TYPE)
public @interface Report {
    int type() default 0;
    String level() default "info";
    String value() default "";
}

在使用的時候,如果一個類用到了@Report:

@Report(type=1)
public class Person {
}

則它的子類默認也定義了該註解:

public class Student extends Person {
}

2.2.如何定義Annotation

我們總結一下定義Annotation的步驟:

2.2.1.第一步,用@interface定義註解:

public @interface Report {
}

2.2.2.第二步,添加參數、默認值:

public @interface Report {
    int type() default 0;
    String level() default "info";
    String value() default "";
}

把最常用的參數定義爲value(),推薦所有參數都儘量設置默認值。

2.2.3.第三步,用元註解配置註解:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Report {
    int type() default 0;
    String level() default "info";
    String value() default "";
}

其中,必須設置@Target和@Retention,@Retention一般設置爲RUNTIME,因爲我們自定義的註解通常要求在運行期讀取。一般情況下,不必寫@Inherited和@Repeatable。

3.處理註解

Java的註解本身對代碼邏輯沒有任何影響。根據@Retention的配置:

  • SOURCE類型的註解在編譯期就被丟掉了;
  • CLASS類型的註解僅保存在class文件中,它們不會被加載進JVM;
  • RUNTIME類型的註解會被加載進JVM,並且在運行期可以被程序讀取。

如何使用註解完全由工具決定。SOURCE類型的註解主要由編譯器使用,因此我們一般只使用,不編寫。CLASS類型的註解主要由底層工具庫使用,涉及到class的加載,一般我們很少用到。只有RUNTIME類型的註解不但要使用,還經常需要編寫
因此,我們只討論如何讀取RUNTIME類型的註解。
因爲註解定義後也是一種class,所有的註解都繼承自java.lang.annotation.Annotation,因此,讀取註解,需要使用反射API。

3.1.Java提供的使用反射API讀取Annotation的方法

3.1.1.判斷某個註解是否存在於Class、Field、Method或Constructor

  • Class.isAnnotationPresent(Class)
  • Field.isAnnotationPresent(Class)
  • Method.isAnnotationPresent(Class)
  • Constructor.isAnnotationPresent(Class)

例如:

// 判斷@Report是否存在於Person類:
Person.class.isAnnotationPresent(Report.class);

3.1.2.使用反射API讀取Annotation

  • Class.getAnnotation(Class)
  • Field.getAnnotation(Class)
  • Method.getAnnotation(Class)
  • Constructor.getAnnotation(Class)

例如:

// 獲取Person定義的@Report註解:
Report report = Person.class.getAnnotation(Report.class);
int type = report.type();
String level = report.level();

使用反射API讀取Annotation有兩種方法。方法一是先判斷Annotation是否存在,如果存在,就直接讀取:

Class cls = Person.class;
if (cls.isAnnotationPresent(Report.class)) {
    Report report = cls.getAnnotation(Report.class);
    ...
}

第二種方法是直接讀取Annotation,如果Annotation不存在,將返回null:

Class cls = Person.class;
Report report = cls.getAnnotation(Report.class);
if (report != null) {
   ...
}

讀取方法、字段和構造方法的Annotation和Class類似。但要讀取方法參數的Annotation就比較麻煩一點,因爲方法參數本身可以看成一個數組,而每個參數又可以定義多個註解,所以,一次獲取方法參數的所有註解就必須用一個二維數組來表示。例如,對於以下方法定義的註解:

public void hello(@NotNull @Range(max=5) String name, @NotNull String prefix) {
}

要讀取方法參數的註解,我們先用反射獲取Method實例,然後讀取方法參數的所有註解:

// 獲取Method實例:
Method m = ...
// 獲取所有參數的Annotation:
Annotation[][] annos = m.getParameterAnnotations();
// 第一個參數(索引爲0)的所有Annotation:
Annotation[] annosOfName = annos[0];
for (Annotation anno : annosOfName) {
    if (anno instanceof Range) { // @Range註解
        Range r = (Range) anno;
    }
    if (anno instanceof NotNull) { // @NotNull註解
        NotNull n = (NotNull) anno;
    }
}

3.2.使用註解

註解如何使用,完全由程序自己決定。例如,JUnit是一個測試框架,它會自動運行所有標記爲@Test的方法。
我們來看一個@Range註解,我們希望用它來定義一個String字段的規則:字段長度滿足@Range的參數定義:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Range {
    int min() default 0;
    int max() default 255;
}

在某個JavaBean中,我們可以使用該註解:

public class Person {
    @Range(min=1, max=20)
    public String name;

    @Range(max=10)
    public String city;
}

但是,定義了註解,本身對程序邏輯沒有任何影響。我們必須自己編寫代碼來使用註解。這裏,我們編寫一個Person實例的檢查方法,它可以檢查Person實例的String字段長度是否滿足@Range的定義:

void check(Person person) throws IllegalArgumentException, ReflectiveOperationException {
    // 遍歷所有Field:
    for (Field field : person.getClass().getFields()) {
        // 獲取Field定義的@Range:
        Range range = field.getAnnotation(Range.class);
        // 如果@Range存在:
        if (range != null) {
            // 獲取Field的值:
            Object value = field.get(person);
            // 如果值是String:
            if (value instanceof String) {
                String s = (String) value;
                // 判斷值是否滿足@Range的min/max:
                if (s.length() < range.min() || s.length() > range.max()) {
                    throw new IllegalArgumentException("Invalid field: " + field.getName());
                }
            }
        }
    }
}

這樣一來,我們通過@Range註解,配合check()方法,就可以完成Person實例的檢查。注意檢查邏輯完全是我們自己編寫的,JVM不會自動給註解添加任何額外的邏輯

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