註解(Annotations)是一種元數據,提供了程序之外的一些程序信息。註解並不會直接影響被註解的代碼。註解有很多用法:
1. 爲編譯器提供信息(Information for the compiler) ——編譯器能直接使用註解檢查錯誤(detect errors)和禁止警告(suppress warnings)。
2. 編譯期和部署時處理(Compile-time and deployment-time processing) —— 軟件工具可以使用註解生成代碼,XML文件等待。
3. 運行期處理(Runtime processing) —— 一些註解可以在運行期被檢測。
這篇教程主要解釋了哪些地方應該使用註解,如何應用註解,如何更好使用Java預定義註解類型;註解如何在支持插件的類型系統,寫出強類型檢測的代碼,以及如何使用重複註解(repeating annotations)。
基礎知識(Annotations Basics)
註解格式
最簡單的註解格式,看起來像下面這樣:
@Entity
標識字母(@)告訴編譯器,緊跟其後的是個註解。下面的例子裏,有個名叫Override的註解:
@Override
void mySuperMethod() { ... }
註解也可以包含元素,元素可以是命名的(named)也可以是未命名的(unnamed),這裏有些元素的值:
@Author(
name = "Benjamin Franklin",
date = "3/27/2003"
)
class MyClass() { ... }
或者
@SuppressWarnings(value = "unchecked")
void myMethod() { ... }
如果僅僅只有一個name爲value的元素,通常name是可以被省略的。像下面這樣:
@SuppressWarnings("unchecked")
void myMethod() { ... }
如果註解沒有元素,則括號也可以被省略,就像上面的@Override例子。
在一個聲明上,使用多個註解也是可以的。
@Author(name = "Jane Doe")
@EBook
class MyClass { ... }
如果多個註解是同一個類型,則被叫做重複註解(repeating annotation)。
@Author(name = "Jane Doe")
@Author(name = "John Smith")
class MyClass { ... }
重複註解是Java SE 8纔開始支持的。後面會有更多詳細信息。
註解類型可以是java.lang或 java.lang.annotation包裏的一個類型。在上面的例子裏,Override 和 SuppressWarnings都是預定義註解( predefined Java annotations)。但我們也可以定義自己的註解類型。上面Author和Ebook便是兩個用戶自定義註解類型。
註解的使用(Where Annotations Can Be Used)
註解可以使用在任何聲明的地方:類,方法,屬性和其他一些程序元素聲明。當聲明被註解時,按照慣例,註解通常獨佔一行。
在Java 8,註解也可以在類型上使用。這裏有一些例子:
1. new 對象的表達式(Class instance creation expression):
new @Interned MyObject();
- 類型轉換(Type cast)
myString = (@NonNull String) str;
- implements 子句(implements clause)
class UnmodifiableList<T> implements
@Readonly List<@Readonly T> { ... }
- 異常拋出聲明(Thrown exception declaration)
void monitorTemperature() throws
@Critical TemperatureException { ... }
上面的這些叫做類型註解。更多詳情,請參見Type Annotations and Pluggable Type Systems
以上內容翻譯自Java官網 註解教程
聲明註解(Declaring an Annotation Type)
代碼中,註解可以代替很多註釋。
假設有個項目,每次開始類的定義時,都需要提供關於類信息的註釋:
public class Generation3List extends Generation2List {
// Author: John Doe
// Date: 3/17/2002
// Current revision: 6
// Last modified: 4/12/2004
// By: Jane Doe
// Reviewers: Alice, Bill, Cindy
// class code goes here
}
爲了使用註解來添加這些元數據(metadata),我們必須先定義註解類型。語法如下:
@interface ClassPreamble {
String author();
String date();
int currentRevision() default 1;
String lastModified() default "N/A";
String lastModifiedBy() default "N/A";
// Note use of array
String[] reviewers();
}
註解的定義和接口比較相似,只是在關鍵字interface前面多了一個@。註解類型是接口(interface)的一種形式,具體內容下面會介紹。現在,我們先不用關注這些。
在上面的註解定義裏,包含了一些看起來像方法(method)的註解元素(annotation type element)的定義。注意一下,他們都可以可選地定義默認值。
在註解類型被定義之後,我們可以用真實值填充相關元素,然後使用它了。
@ClassPreamble (
author = "John Doe",
date = "3/17/2002",
currentRevision = 6,
lastModified = "4/12/2004",
lastModifiedBy = "Jane Doe",
// Note array notation
reviewers = {"Alice", "Bob", "Cindy"}
)
public class Generation3List extends Generation2List {
// class code goes here
}
注意: 爲了使註解@ClassPreamble能夠出現在Javadoc自動生成的文檔裏,我們在@ClassPreamble定義時,使用註解@Documented:
// import this to use @Documented
import java.lang.annotation.*;
@Documented
@interface ClassPreamble {
// Annotation element definitions
}
以上內容翻譯自Java官網 declaring
內置預定義註解(Predefined Annotation Types)
Java SE API裏內置了很多註解。一些註解是爲了編譯器使用的,還有一些是爲了給其他註解使用的。
Java編譯器使用的註解(Annotation Types Used by the Java Language)
在java.lang裏有@Deprecated, @Override 和 @SuppressWarnings,都屬於這一類型。
@Deprecated
@Deprecated註解表示被標記的元素時被廢棄的(deprecated),不應該再被使用。當代碼使用被@Deprecated註解的方法,屬性或者類的時候,編譯器會給出警告(warning)。當一個元素被廢棄了,它將會在Javadoc文檔裏打上@deprecated標記(@deprecated tag),就像下面的例子一樣。Javadoc註釋和註解都使用@開頭,並不是巧合,他們在概念上是相關聯的。當然,我們也會看到Javadoc tag是以小寫字母d開頭,而註解是以大寫字母D開頭。
// Javadoc comment follows
/**
* @deprecated
* explanation of why it was deprecated
*/
@Deprecated
static void deprecatedMethod() { }
@Override
@Override 註解告訴編譯器被註解的元素時重寫(override)父類的。關於重寫方法的信息,可以參見接口與繼承(Interfaces and Inheritance)
// mark method as a superclass method
// that has been overridden
@Override
int overriddenMethod() { }
重寫方法,並不是強制需要這個註解,但這個註解可以幫助避免錯誤。如果一個被@Override標記的方法,沒有正確重寫父類的方法,編譯器會報錯。
@SuppressWarnings
@SuppressWarnings註解告訴編譯器不要產生某個的警告。在下面的例子裏,使用了一個被廢棄(deprecated)的方法,然後編譯器會產生一個警告(warning)。在這種情況下,這個註解要會抑制編譯器產生警告。
// use a deprecated method and tell
// compiler not to generate a warning
@SuppressWarnings("deprecation")
void useDeprecatedMethod() {
// deprecation warning
// - suppressed
objectOne.deprecatedMethod();
}
每個編譯期警告都有一個類型。Java語言規範列出了兩種類型:deprecation 和unchecked。當接口是使用泛型前的遺留代碼時,就會產生unchecked警告。爲了抑制多個類型的警告,可以使用下面語法:
@SuppressWarnings({"unchecked", "deprecation"})
@SafeVarargs @SafeVarargs註解應用於一個方法或者構造函數,表示代碼不會在可變參數(varargs)上進行不安全的操作。當@SafeVarargs註解被使用時,與可變參數相關(varargs)的unchecked警告都會被抑制。
@FunctionalInterface @FunctionalInterface註解是Java 8新加入的一個註解,用於表示類型是打算用於函數式接口的。
用於其他註解的註解(Annotations That Apply to Other Annotations)
註解用於其他的註解被叫做元註解(meta-annotations)。在java.lang.annotation裏定義了很多元註解。
@Retention
@Retention註解指明被標記的註解如何存儲:
1. RetentionPolicy.SOURCE 表示被標記的註解,僅僅只在代碼級被保留,同時會被編譯器忽略;
2. RetentionPolicy.CLASS 該標記表示被標記的註解會被編譯器保留,但僅僅在編譯期保留,但是會被JVM忽略;
3. RetentionPolicy.RUNTIME 該標記的註解,表示會被JVM保留,可以在運行時被使用。
@Documented
@Documented註解指明被註解的元素,會被Javadoc工具生成到Javadoc文檔裏(默認,註解是不會包含在Javadoc裏的)。更多信息,可以參見Javadoc工具頁。
@Target
@Target註解用於註解可以用於哪些元素。一個target註解可以指定下面元素類型值的一種:
1. ElementType.ANNOTATION_TYPE 應用於一個註解類型
2. ElementType.CONSTRUCTOR 應用於構造函數
3. ElementType.FIELD 應用於類的屬性
4. ElementType.LOCAL_VARIABLE 應用於局部變量
5. ElementType.METHOD 應用於方法
6. ElementType.PACKAGE 應用於包
7. ElementType.PARAMETER 應用於方法的參數
8. ElementType.TYPE 應用於類的元素
@Inherited
@Inherited註解表示這個註解可以被從父類繼承(默認是不可以的)。當用戶判斷類是否被註解時,同時類並沒有直接被註解,這時會去查詢父類是否被註解。@Inherited註解只能在class上使用。
@Repeatable
@Repeatable註解是被Java 8新引入的,表示註解可以在同一個聲明或類型上使用多次。
以上內容翻譯自Java 官網教程 內置註解
Type註解和插件(Pluggable Type Systems)
在Java 8之前,註解僅僅只能在聲明(declarations)時使用。在Java 8,註解可以在任何 type use。這就是說,註解可以應用在任何你使用type的地方。例如,class實例的創建表達式(new),類型轉換(casts),implements子句,throws子句。這種註解被稱爲type annotation。更多例子,可以參考上面的基礎知識(Annotations Basics)。
Type annotation是爲了提高Java程序的強類型檢查功能而誕生的。Java 8並沒有提供一個類型檢查框架(type checking framework),但卻允許你自己寫(或者down)一個類型檢查框架作爲Java編譯器的插件使用。
舉個栗子,你想確保你程序裏的某些變量永遠不會被賦值爲null;你想避免產生NullPointerException。你可以自己寫個插件來檢查。你只需要修改你的代碼,把特定變量加上標識不能用null賦值的註解。這個變量的聲明可能看起來像這樣:
@NonNull String str;
當你使用NonNull模塊編譯這個代碼時,編譯器會在會檢測到潛在問題時,產生一個異常,從而提醒你修改代碼而避免這個錯誤。在你改正代碼清除所有警告後,這個特定的錯誤便不會在程序運行時產生。
你也可以使用多個檢查不同種類錯誤的類型檢查(type-checking)模塊。在這種方式下,你可以在任何時候在Java類型系統(Java type system)的任何地方添加特別的檢測。
恰當的使用類型註解和類型檢查插件,我們可以寫出更健壯和不易出錯的代碼。
在很多情況下,我們都不用自己寫類型檢查模塊。很多第三方都已經爲我們寫好了這些。例如,你可以使用華盛頓大學(University of Washington)的the Checker Framework。這個框架包含了NonNull模塊,同時還有正則表達式檢測模塊,互斥鎖模塊。更多信息,可以參見the Checker Framework的官網
以上內容翻譯自Java 官網
重複註解(Repeating Annotations)
很多時候,我們想在一個聲明或者類型上多次使用同一個註解。在Java 8中,重複註解(Repeating Annotations)便提供了我們需要的這種功能。
比如,我們編寫一個通過使用類似unix cron的定時器服務調度的方法。現在我們可以設置一個定時器來運行方法,doPeriodicCleanup,在每月月末和每個週五晚上的23:00時刻運行。爲了設置定時器,可以創建一個@Schedule註解,並且在doPeriodicCleanup方法上使用兩次。第一次表示每月月末,第二次表示每週五晚上的23:00。像下面這樣:
@Schedule(dayOfMonth="last")
@Schedule(dayOfWeek="Fri", hour="23")
public void doPeriodicCleanup() { ... }
上面的例子,是對方法進行註解。你可以對任何可以使用註解的地方進行重複註解。例如,我們有個用於處理未登陸訪問異常的class。我們可以使用@Alert註解來表示類的超級管理員和普通管理員。
@Alert(role="Manager")
@Alert(role="Administrator")
public class UnauthorizedAccessException extends SecurityException { ... }
由於兼容性的原因,重複註解(repeating annotations)存儲在一個Java編譯器自動生成的註解容器(container annotation)裏。爲了編譯器能做這些事情,在我們的代碼裏需要進行下面兩步。
步驟一:定義一個重複註解類型(Declare a Repeatable Annotation Type)
註解類型必須被元註解@Repeatable所標記。下面是個自定義重複註解類型@Schedule的定義:
import java.lang.annotation.Repeatable;
@Repeatable(Schedules.class)
public @interface Schedule {
String dayOfMonth() default "first";
String dayOfWeek() default "Mon";
int hour() default 12;
}
元註解@Repeatable括號內的值,便是Java編譯器自動生成的用於存儲重複註解(repeating annotations)的註解容器(container annotation)的類型。在這個例子裏,這注解容器的類型便是Schedules。
任何沒有進行步驟一的註解,如果在同個地方使用兩次,都會產生一個編譯錯誤。
步驟二:定義註解容器類型(Declare the Containing Annotation Type)
註解容器類型必須擁有一個返回數組的value方法。Schedules類型註解容器的聲明:
public @interface Schedules {
Schedule[] value();
}
Retrieving Annotations
在Java反射(Reflection)的API有很多方法用來獲取註解信息。有些方法返回一個註解,例如:AnnotatedElement.getAnnotationByType(Class<T>),如果提供的類型擁有註解,則會原封不動地返回。如果一個元素有多個註解,則可以通過註解容器獲得。在這種方式下,之前的遺留代碼仍然可以兼容。另外一些方法是Java SE 8新添加的,可以一次性返回多個註解,例如 AnnotatedElement.getAnnotations(Class<T>)。更多信息可以參見AnnotatedElement類
注意事項(Design Considerations)
當設計一個註解時,我們必須考慮註解的使用次數。現在註解,可以使用零次,一次或者通過@Repeatable來實現多次使用。我們也可以使用@Target來實現在哪裏註解使用的限制。例如,我們可以創建一個重複註解,只能使用在方法和屬性上。我們必須非常小心的設計自定義註解,儘可能使註解靈活和強大。
以上內容翻譯自Java 官網Repeating Annotations
問題和練習題(Questions and Exercises)
問題一
下面接口有什麼問題呢?
public interface House {
@Deprecated
void open();
void openFrontDoor();
void openBackDoor();
}
問題二
假設MyHouse類implements上面問題一的House接口
public class MyHouse implements House {
public void open() {}
public void openFrontDoor() {}
public void openBackDoor() {}
}
編譯這個代碼會有一個警告,因爲open是deprecated的,如果才能避免這個警告呢
問題三
下面的代碼能編譯通過嗎?原因是什麼?
public @interface Meal { ... }
@Meal("breakfast", mainDish="cereal")
@Meal("lunch", mainDish="pizza")
@Meal("dinner", mainDish="salad")
public void evaluateDiet() { ... }
問題四
定義一個擁有四個元素id, synopsis, engineer, 和date的註解;同時,指定engineer的默認值爲unassigned,date的默認值是unknown。
答案一
上面的open函數應該指出爲啥open函數被deprecated和使用什麼函數來代替它,例如:
public interface House {
/**
* @deprecated use of open
* is discouraged, use
* openFrontDoor or
* openBackDoor instead.
* open 函數已經Deprecated,
* 使用openFrontDoor或
* openBackDoor來代替它
*/
@Deprecated
public void open();
public void openFrontDoor();
public void openBackDoor();
}
答案二
有兩種方式,第一種,添加@Deprecated註解:
public class MyHouse implements House {
// The documentation is
// inherited from the interface.
@Deprecated
public void open() {}
public void openFrontDoor() {}
public void openBackDoor() {}
}
另外,也可以使用@SuppressWarnings 註解:
public class MyHouse implements House {
@SuppressWarnings("deprecation")
public void open() {}
public void openFrontDoor() {}
public void openBackDoor() {}
}
答案三
代碼是編譯不成功的。在JDK 8之前,重複註解是不支持的。在JDK 8之後,代碼還是編譯不成功。因爲Meal註解沒有定義爲可重複的(repeatable)。可以修改Meal的定義
@java.lang.annotation.Repeatable(MealContainer.class)
public @interface Meal { ... }
public @interface MealContainer {
Meal[] value();
}
答案四
代碼如下:
/**
* Describes the Request-for-Enhancement (RFE) annotation type.
*/
public @interface RequestForEnhancement {
int id();
String synopsis();
String engineer() default "[unassigned]";
String date() default "[unknown]";
}