Java 註解 (Annotation)淺入深出
本文主要參考與借鑑frank909 文章,但更為簡單,詳細。
Annotation 中文譯過來就是註解、標釋的意思。Annotation是一種應用於類、方法、引數、變數、構造器及包宣告中的特殊修飾符。它是一種由JSR-175標準選擇用來描述後設資料的一種工具。 在 Java 中註解是一個很重要的知識點,註解目前非常的流行,很多主流框架都支援註解,而且自己編寫程式碼的時候也會盡量的去用註解,一是方便,二是程式碼更加簡潔。
註解語法
因為平常開發少見,相信有不少的人員會認為註解的地位不高。其實同 classs 和 interface 一樣,註解也屬於一種型別。它是在 Java SE 5.0 版本中開始引入的概念。
package java.lang;
import java.lang.annotation.*;
/**
* Indicates that a method declaration is intended to override a
* method declaration in a supertype. If a method is annotated with
* this annotation type compilers are required to generate an error
* message unless at least one of the following conditions hold:
*
* <ul><li>
* The method does override or implement a method declared in a
* supertype.
* </li><li>
* The method has a signature that is override-equivalent to that of
* any public method declared in {@linkplain Object}.
* </li></ul>
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
複製程式碼
它的形式跟介面很類似,不過前面多了一個 @ 符號。上面的程式碼就建立了一個名字為 Override的註解。
你可以簡單理解為建立了一張名字為 Override的標籤。
**Annotations僅僅是後設資料,和業務邏輯無關。**理解起來有點困難,但就是這樣。如果Annotations不包含業務邏輯,那麼必須有人來實現這些邏輯。後設資料的使用者來做這個事情。Annotations僅僅提供它定義的屬性(類/方法/包/域)的資訊。Annotations的使用者(同樣是一些程式碼)來讀取這些資訊並實現必要的邏輯。
元註解
元註解是什麼意思呢?
元註解是可以註解到註解上的註解,或者說元註解是一種基本註解,但是它能夠應用到其它的註解上面。
如果難於理解的話,你可以這樣理解。元註解也是一張標籤,但是它是一張特殊的標籤,它的作用和目的就是給其他普通的標籤進行解釋說明的。
元標籤有 @Retention、@Documented、@Target(當一個註解被 @Target 註解時,這個註解就被限定了運用的場景 )、@Inherited、@Repeatable 5 種。
@target | 表示該註解可以用於什麼地方,可能的ElementType引數有: CONSTRUCTOR:構造器的宣告 FIELD:域宣告(包括enum例項) LOCAL_VARIABLE:區域性變數宣告 METHOD:方法宣告 PACKAGE:包宣告 PARAMETER:引數宣告 TYPE:類、介面(包括註解型別)或enum宣告 |
---|---|
@Retention | 表示需要在什麼級別儲存該註解資訊。可選的RetentionPolicy引數包括: SOURCE:註解將被編譯器丟棄; CLASS:註解在class檔案中可用,但會被VM丟棄; RUNTIME:VM將在執行期間保留註解,因此可以通過反射機制讀取註解的資訊。 |
@Document | 將註解包含在Javadoc中 |
@Inherited | 允許子類繼承父類中的註解 |
@Repeatable | 可重複 (@Repeatable 是 Java 1.8) |
註解的屬性
註解的屬性也叫做成員變數。註解只有成員變數,沒有方法。註解的成員變數在註解的定義中以“無形參的方法”形式來宣告,其方法名定義了該成員變數的名字,其返回值定義了該成員變數的型別。需要注意的是,在註解中定義屬性時它的型別必須是 8 種基本資料型別外加 類、介面、註解及它們的陣列。
註解中屬性可以有預設值,預設值需要用 default 關鍵值指定。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Person {
int id() default -10998;
String msg() default "no hello";
}
複製程式碼
上面程式碼定義了 Person這個註解中擁有 id 和 msg 兩個屬性。在使用的時候,我們應該給它們進行賦值。賦值的方式是在註解的括號內以 value=”” 形式,多個屬性之前用 ,隔開。 一個註解內僅僅只有一個名字為 value 的屬性時,這個註解時可以直接接屬性值填寫到括號內。 還需要注意的一種情況是一個註解沒有任何屬性 括號可以省略。。
public @interface NoUse {
}
複製程式碼
public @interface Chou {
String value() default "You";
}
複製程式碼
@Person(id = 10758, msg = "hello android")//或者直接預設@Person()
public class Liming {
@Chou("She")
String beautiful;
@NoUse
public void say() {
}
}
複製程式碼
Java 預置的註解
Java 語言本身已經提供了幾個現成的註解。
@Deprecated
這個元素是用來標記過時的元素,想必大家在日常開發中經常碰到。編譯器在編譯階段遇到這個註解時會發出提醒警告,告訴開發者正在呼叫一個過時的元素比如過時的方法、過時的類、過時的成員變數。
@Person(id = 10758, msg = "hello android")//或者直接預設@Person()
public class Liming {
@Chou("She")
String beautiful;
@NoUse
public void say() {
System.out.println(" say is using ");
}
@Deprecated
public void speak() {
System.out.println(" speak is out of date ");
}
}
複製程式碼
Liming類,它有兩個方法 say() 和 speak() ,其中 speak() 被 @Deprecated 註解。然後我們在 IDE 中分別呼叫它們。
可以看到,speak() 方法上面被一條直線劃了一條,這其實就是編譯器識別後的提醒效果。
@SuppressWarnings
阻止警告。呼叫被 @Deprecated 註解的方法後,編譯器會警告提醒,而有時候開發者會忽略這種警告,他們可以在呼叫的地方通過 @SuppressWarnings 達到目的。
@SafeVarargs
引數安全型別註解。它的目的是提醒開發者不要用引數做一些不安全的操作,它的存在會阻止編譯器產生 unchecked 這樣的警告,在 Java 1.7 的版本中加入。
上面的程式碼中,編譯階段不會報錯,執行時會丟擲 ClassCastException 這個異常。
@FunctionalInterface
函式式介面註解,這個是 Java 1.8 版本引入的新特性。
函式式介面 (Functional Interface) 就是一個具有一個方法的普通介面。
@FunctionalInterface
public interface Runnable {
/**
* When an object implementing interface <code>Runnable</code> is used
* to create a thread, starting the thread causes the object's
* <code>run</code> method to be called in that separately executing
* thread.
* <p>
* The general contract of the method <code>run</code> is that it may
* take any action whatsoever.
*
* @see java.lang.Thread#run()
*/
public abstract void run();
}
複製程式碼
我們進行執行緒開發中常用的 Runnable 就是一個典型的函式式介面,上面原始碼可以看到它就被 @FunctionalInterface 註解。
可能有人會疑惑,函式式介面標記,函式式介面可以很容易轉換為 Lambda 表示式。
註解與反射
- 註解通過反射獲取。首先可以通過 Class 物件的 isAnnotationPresent() 方法判斷它是否應用了某個註解
public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {}
- 然後通過 getAnnotation() 方法來獲取 Annotation 物件。
public <A extends Annotation> A getAnnotation(Class<A> annotationClass) {}
- 或者是 getAnnotations() 方法。
public Annotation[] getAnnotations() {}
- 前一種方法返回指定型別的註解,後一種方法返回註解到這個元素上的所有註解。
如果獲取到的 Annotation 如果不為 null,則就可以呼叫它們的屬性方法了。
屬性、方法上的註解照樣是可以的。同樣還是要藉助於反射。
public static void getAnnotation() {
boolean hasAnnotation = MainActivity.class.isAnnotationPresent(Person.class);
if (hasAnnotation) {
Person testPerson = MainActivity.class.getAnnotation(Person.class);
System.out.println("id is " + testPerson.id() + " msg is " + testPerson.msg());
}
}
public static void getField() {
try {
Field a = Liming.class.getDeclaredField("beautiful");
a.setAccessible(true);
Chou chou = a.getAnnotation(Chou.class);
if (chou != null) {
System.out.println("check value:" + chou.value());
}
Method noUse = Liming.class.getDeclaredMethod("say");
if (noUse != null) { // 獲取方法中的註解
Annotation[] ans = noUse.getAnnotations();
for (int i = 0; i < ans.length; i++) {
System.out.println("method noUse annotation:" + ans[i].annotationType().getSimpleName());
}
}
} catch (NoSuchFieldException e) {
e.printStackTrace();
System.out.println("NoSuchFieldException");
} catch (NoSuchMethodException e) {
e.printStackTrace();
System.out.println("NoSuchMethodException");
}
}
複製程式碼
say is using
speak is out of date
id is -10998 msg is this is not default
check value:She
method noUse annotation:NoUse
Process finished with exit code 0
複製程式碼
當開發者使用了Annotation 修飾了類、方法、Field 等成員之後,這些 Annotation 不會自己生效,必須由開發者提供相應的程式碼來提取並處理 Annotation 資訊。這些處理提取和處理 Annotation 的程式碼統稱為 APT(Annotation Processing Tool)。
註解是一系列後設資料,它提供資料用來解釋程式程式碼,但是註解並非是所解釋的程式碼本身的一部分。註解對於程式碼的執行效果沒有直接影響。
註解有許多用處,主要如下:
- 提供資訊給編譯器: 編譯器可以利用註解來探測錯誤和警告資訊
- 編譯階段時的處理: 軟體工具可以用來利用註解資訊來生成程式碼、Html文件或者做其它相應處理。
- 執行時的處理: 某些註解可以在程式執行的時候接受程式碼的提取
複製程式碼
親手自定義註解完成某個目的
需求:自定義註解與實現,檢查MaSaGei類中的錯誤並反饋
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckOut {
}
複製程式碼
public class MaSaGei {
@CheckOut
public void testOne() {
System.out.println(" 1 + 0 = " + ((1 + 1) / 2));
}
@CheckOut
public void testTwo() {
System.out.println(" 1 + 1 = " + (8 / 4));
}
@CheckOut
public void testThree() {
System.out.println(" 1 + 2 = " + (6 / 2));
}
@CheckOut
public void testFour() {
System.out.println(" 1 / 3 = " + (6 / 0));
}
}
複製程式碼
public class CheckOutTool {
public static void checkAll() {
MaSaGei maSaGei = new MaSaGei();
Class clazz = maSaGei.getClass();
Method[] method = clazz.getDeclaredMethods();
StringBuilder log = new StringBuilder();
// 記錄異常的次數
int errornum = 0;
for (Method m : method) {
if (m.isAnnotationPresent(CheckOut.class)) {
m.setAccessible(true);
try {
m.invoke(maSaGei, null);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
errornum++;
log.append(m.getName());
log.append(" ");
log.append("has error ");
log.append("\\n\\r caused by ");
log.append(e.getCause().getClass().getSimpleName());
log.append("\n\r");
log.append(e.getCause().getMessage());
log.append("\n\r");
}
}
}
log.append(clazz.getSimpleName());
log.append(" has ");
log.append(errornum);
log.append(" error."); // 生成測試報告
System.out.println(log.toString());
}
}
複製程式碼
結果如下:
testFour has error \n\r caused by ArithmeticException
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.heng.subhey.annotation.CheckOutTool.checkAll(CheckOutTool.java:18)
at com.heng.subhey.MainActivity.main(MainActivity.java:22)
Caused by: java.lang.ArithmeticException: / by zero
/ by zero
at com.heng.subhey.annotation.MaSaGei.testFour(MaSaGei.java:21)
... 6 more
MaSaGei has 1 error.
Process finished with exit code 0
複製程式碼
註解應用例項
JUnit
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() throws Exception {
assertEquals(4, 2 + 2);
}
}
複製程式碼
@Test 標記了要進行測試的方法 addition_isCorrect()。
ButterKnife
ButterKnife 是 Android 開發中大名鼎鼎的 IOC 框架,它減少了大量重複的程式碼。
public class GoPayActivity extends BaseActivity {
@BindView(R.id.title_tv)
TextView title_tv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
title_tv.setText("支付");
}
@Override
protected int getLayoutId() {
return R.layout.activity_go_pay;
}
@OnClick({R.id.title_back, R.id.pay_confirm})
public void onClick(View v) {
switch (v.getId()) {
case R.id.title_back:
finish();
break;
case R.id.pay_confirm:
ToastUtils.showLong("支付環境安全檢測中...");
title_tv.postDelayed(new Runnable() {
@Override
public void run() {
ToastUtils.showShort("即將跳轉至支付頁面");
Intent charge = new Intent(GoPayActivity.this, KeyWriteActivity.class);
startActivity(charge);
}
}, 2 * 1000);
break;
}
}
}
複製程式碼
Retrofit
Http 網路訪問框架
public interface GitHubService { @GET("users/{user}/repos")
Call<List<Repo>> listRepos(@Path("user") String user); }
Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://api.github.com/") .build(); GitHubService service = retrofit.create(GitHubService.class);
複製程式碼
總結
- 註解難於理解,註解為了解釋程式碼。
- 註解的基本語法,多了個 @ 符號。
- 註解的元註解。
- 註解的屬性。
- 註解主要給編譯器及工具型別的軟體用的。
- 註解的提取需要藉助於 Java 的反射技術,反射比較慢,所以註解使用時也需要謹慎計較時間成本。