Java 註解 (Annotation)淺入深出

愛學習程式猿發表於2018-12-05

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 這個異常。

20181205141508.png

@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 的反射技術,反射比較慢,所以註解使用時也需要謹慎計較時間成本。

相關文章