在 JDK8 出現之前,ElementType
的列舉成員 TYPE
、FIELD
、METHOD
、PARAMETER
、CONSTRUCTOR
、LOCAL_VARIABLE
、ANNOTATION_TYPE
、PACKAGE
等,是用來限定哪個宣告位置可以進行標註。
JDK8 的 ElementType
多了兩個列舉成員 TYPE_PARAMETER
、TYPE_USE
,它們是用來限定哪個型態可以進行標註。舉例來說,如果想要對泛型的型態參數(Type parameter)進行標註:
public class MailBox<@Email T> {
...
}
那麼,你在定義 @Email
時,必須在 @Target
設定 ElementType.TYPE_PARAMETER
,表示這個標註可用來標註型態參數。例如:
package cc.openhome;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
@Target(ElementType.TYPE_PARAMETER)
public @interface Email {}
ElementType.TYPE_USE
可用於標註在各式型態,因此上面的範例也可以將 ElementType.TYPE_PARAMETER
改為 ElementType.TYPE_USE
,一個標註如果被設定為 ElementType.TYPE_USE
,只要是型態名稱,都可以進行標註。例如若有個標註定義如下:
package cc.openhome;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
@Target(ElementType.TYPE_USE)
public @interface Test {}
那以下幾個標註範例都是可以的:
List<@Test Comparable> list1 = new ArrayList<>();
List<? extends Comparable> list2 = new ArrayList<@Test Comparable>();
@Test String text;
text = (@Test String) new Object();
java.util. @Test Scanner console;
console = new java. util. @Test11 Scanner(System.in);
注意,這幾個範例都僅對 @Test
右邊的型態名稱進行標註,你得與 JDK8 出現前就存在的列舉成員 TYPE
、FIELD
、METHOD
、PARAMETER
、CONSTRUCTOR
、LOCAL_VARIABLE
、ANNOTATION_TYPE
、PACKAGE
等區別。舉例來說,以下的標註就不合法:
@Test java.lang.String text;
上面這個例子中,java.lang.String text
顯然是在進行 text
變數的宣告,如果是在宣告一個區域變數,想要讓以上合法,@Test
得在 @Target
加註 ElementType.LOCAL_VARIABLE
。
可以在更多地方標註,一些靜態分析工具或框架是最主要受到影響的對象,舉例來說,The Checker Framework 中有個 @NonNull
標註,@Target
就設定為 TYPE_USE
與 TYPE_PARAMETER
:
...
@Retention(value=RUNTIME)
@Target(value={TYPE_USE,TYPE_PARAMETER})
public @interface NonNull
...
你可以 下載 The Checker Framework,撰寫本文的時間點它是 1.8.1 版,下載完成後解開 zip 檔案,並設置環境變數:
SET CHECKERFRAMEWORK=%YOUR_WORKSPACE%\checker-framework-1.8.1
SET PATH=%CHECKERFRAMEWORK%\checker\bin;%PATH%
最主要的是,你的 PATH
必須包括解開後的 zip 中 checker\bin 目錄。你可以撰寫一個簡單的程式:
package cc.openhome;
import org.checkerframework.checker.nullness.qual.*;
public class GetStarted {
public static void main() {
java.util.@NonNull List<String> elems =
new java.util.ArrayList<>();
}
}
這個簡單的程式使用了 @NonNull
標註,表明 elems
不能是 null
,如果你使用以下指令進行編譯:
javac -processor org.checkerframework.checker.nullness.NullnessChecker GetStarted.java
程式中的 elems
因為不為 null
,所以不會發生編譯錯誤,如果你將之改為:
java.util.@NonNull List<String> elems = null;
使用相同指令編譯時,就會發生以下編譯錯誤:
error: [assignment.type.incompatible] incompatible types in assignment.
java.util.@NonNull List<String> elems = null;
^
found : null
required: @UnknownInitialization @NonNull List<@Initialized @NonNull String>
1 error
可以看到,對於 List
,Checker 框架會預設不能收集 null
,因此,如果你撰寫以下程式:
package cc.openhome;
import org.checkerframework.checker.nullness.qual.*;
import java.util.*;
public class GetStarted {
public static void main(String[] args) {
List<String> elems = new ArrayList<>();
elems.add(null);
}
}
使用相同指令編譯,預設會檢查出被加入 null
元素而發生編譯錯誤:
error: [argument.type.incompatible] incompatible types in argument.
elems.add(null);
^
found : null
required: @Initialized @NonNull String
1 error
如果你真的想允許 List
可以收集 null
,那麼可以加以標註,那麼使用相同指令編譯時,就不會發生編譯錯誤:
List<@Nullable String> elems = new ArrayList<>();
elems.add(null);
想要知道更多 Checker 的使用,可以參考The Checker Framework Manual。
JDK8 除了 ElementType
多了兩個列舉成員 TYPE_PARAMETER
、TYPE_USE
之外,還新增了個 @Repeatable
,可以讓你在同一個位置重複相同標註。舉例來說,你也許本來定義了以下的 @Filter
標註:
public @interface Filter {
String[] value();
}
這可以讓你如下進行標註:
@Filter({"/admin", "/manager"})
public interface SecurityFilter {
...
}
如果你想要另一種如下的標註風格:
package cc.openhome;
@Filter("/admin")
@Filter("/manager")
public interface SecurityFilter {}
在 JDK8 還沒出現之前,沒有辦法達到這點需求,如果使用 JDK8,可以如下定義 @Filter
來解決這類問題:
package cc.openhome;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(Filters.class)
public @interface Filter {
String value();
}
@Retention(RetentionPolicy.RUNTIME)
@interface Filters {
Filter[] value();
}
實際上這是編譯器的把戲,在這邊你使用 @Repeatable
時告訴編譯器,使用 @Filters
來作為收集重複標註資訊的容器,而每個 @Filter
儲存各自指定的字串值。
JDK8 在 java.lang.reflect.AnnotatedElement
新增了 getDeclaredAnnotationsByType
、getAnnotationsByType
,在指定 @Repeatable
的標註時,會找尋收集重複標註的容器中,相對來說,getDeclaredAnnotation
與 getAnnotation
就不會處理 @Repeatable
的標記。舉例來說,可以使用以下範例,來讀取之前看過的 SecurityFilter
上的重複的 @Filter
標記資訊:
package cc.openhome;
import static java.lang.System.out;
public class SecurityTool {
public static void main(String[] args) {
Filter[] filters = SecurityFilter.class.
getAnnotationsByType(Filter.class);
for(Filter filter : filters) {
out.println(filter.value());
}
out.println(SecurityFilter.class.getAnnotation(Filter.class));
}
}
執行結果如下,可以觀察到,對於被標註為 @Repeatable
的 @Filter
,getAnnotation
傳回值會是 null
:
/admin
/manager
null