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. * * @seejava.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\\rcaused 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()); } } 複製程式碼
結果如下:
testFourhas error \n\rcaused 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 has1 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/">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 的反射技術,反射比較慢,所以註解使用時也需要謹慎計較時間成本。