Effective Java 第三版——39. 註解優於命名模式

Tips
《Effective Java, Third Edition》一書英文版已經出版,這本書的第二版想必不少人都讀過,號稱Java四大名著之一,不過第二版2009年出版,到如今已經將近8年的時間,但隨着Java 6,7,8,甚至9的發佈,Java語言發生了深入的變化。
在這裏第一時間翻譯成中文版。供你們學習分享之用。java

Effective Java, Third Edition

39. 註解優於命名模式

過去,一般使用命名模式( naming patterns)來指示某些程序元素須要經過工具或框架進行特殊處理。 例如,在第4版以前,JUnit測試框架要求其用戶經過以test[Beck04]開始名稱來指定測試方法。 這種技術是有效的,但它有幾個很大的缺點。 首先,拼寫錯誤致使失敗,但不會提示。 例如,假設意外地命名了測試方法tsetSafetyOverride而不是testSafetyOverride。 JUnit 3不會報錯,但它也不會執行測試,致使錯誤的安全感。程序員

命名模式的第二個缺點是沒法確保它們僅用於適當的程序元素。 例如,假設調用了TestSafetyMechanisms類,但願JUnit 3可以自動測試其全部方法,而無論它們的名稱如何。 一樣,JUnit 3也不會出錯,但它也不會執行測試。數組

命名模式的第三個缺點是它們沒有提供將參數值與程序元素相關聯的好的方法。 例如,假設想支持只有在拋出特定異常時才能成功的測試類別。 異常類型基本上是測試的一個參數。 你可使用一些精心設計的命名模式將異常類型名稱編碼到測試方法名稱中,但這會變得醜陋和脆弱(條目 62)。 編譯器沒法知道要檢查應該命名爲異常的字符串是否確實存在。 若是命名的類不存在或者不是異常,那麼直到嘗試運行測試時纔會發現。安全

註解[JLS,9.7]很好地解決了全部這些問題,JUnit從第4版開始採用它們。在這個項目中,咱們將編寫咱們本身的測試框架來顯示註解的工做方式。 假設你想定義一個註解類型來指定自動運行的簡單測試,而且若是它們拋出一個異常就會失敗。 如下是名爲Test的這種註解類型的定義:app

// Marker annotation type declaration

import java.lang.annotation.*;



/**

 * Indicates that the annotated method is a test method.

 * Use only on parameterless static methods.

 */

@Retention(RetentionPolicy.RUNTIME)

@Target(ElementType.METHOD)

public @interface Test {

}

Test註解類型的聲明自己使用RetentionTarget註解進行標記。 註解類型聲明上的這種註解稱爲元註解。 @Retention(RetentionPolicy.RUNTIME)元註解指示Test註解應該在運行時保留。 沒有它,測試工具就不會看到Test註解。@Target.get(ElementType.METHOD)元註解代表Test註解只對方法聲明合法:它不能應用於類聲明,屬性聲明或其餘程序元素。框架

在Test註解聲明以前的註釋說:「僅在無參靜態方法中使用」。若是編譯器能夠強制執行此操做是最好的,但它不能,除非編寫註解處理器來執行此操做。 有關此主題的更多信息,請參閱javax.annotation.processing文檔。 在缺乏這種註解處理器的狀況下,若是將Test註解放在實例方法聲明或帶有一個或多個參數的方法上,那麼測試程序仍然會編譯,並將其留給測試工具在運行時來處理這個問題 。less

如下是Test註解在實踐中的應用。 它被稱爲標記註解,由於它沒有參數,只是「標記」註解元素。 若是程序員錯拼Test或將Test註解應用於程序元素而不是方法聲明,則該程序將沒法編譯。ide

// Program containing marker annotations

public class Sample {

    @Test public static void m1() { }  // Test should pass

    public static void m2() { }

    @Test public static void m3() {     // Test should fail

        throw new RuntimeException("Boom");

    }

    public static void m4() { }

    @Test public void m5() { } // INVALID USE: nonstatic method

    public static void m6() { }

    @Test public static void m7() {    // Test should fail

        throw new RuntimeException("Crash");

    }

    public static void m8() { }

}

Sample類有七個靜態方法,其中四個被標註爲Test。 其中兩個,m3和m7引起異常,兩個m1和m5不引起異常。 可是沒有引起異常的註解方法之一是實例方法,所以它不是註釋的有效用法。 總之,Sample包含四個測試:一個會經過,兩個會失敗,一個是無效的。 未使用Test註解標註的四種方法將被測試工具忽略。工具

Test註解對Sample類的語義沒有直接影響。 他們只提供信息供相關程序使用。 更通常地說,註解不會改變註解代碼的語義,但能夠經過諸如這個簡單的測試運行器等工具對其進行特殊處理:學習

// Program to process marker annotations

import java.lang.reflect.*;



public class RunTests {

    public static void main(String[] args) throws Exception {

        int tests = 0;

        int passed = 0;

        Class<?> testClass = Class.forName(args[0]);

        for (Method m : testClass.getDeclaredMethods()) {

            if (m.isAnnotationPresent(Test.class)) {

                tests++;

                try {

                    m.invoke(null);

                    passed++;

                } catch (InvocationTargetException wrappedExc) {

                    Throwable exc = wrappedExc.getCause();

                    System.out.println(m + " failed: " + exc);

                } catch (Exception exc) {

                    System.out.println("Invalid @Test: " + m);

                }

            }

        }

        System.out.printf("Passed: %d, Failed: %d%n",

                          passed, tests - passed);

    }

}

測試運行器工具在命令行上接受徹底限定的類名,並經過調用Method.invoke來反射地運行全部類標記有Test註解的方法。 isAnnotationPresent方法告訴工具要運行哪些方法。 若是測試方法引起異常,則反射機制將其封裝在InvocationTargetException中。 該工具捕獲此異常並打印包含由test方法拋出的原始異常的故障報告,該方法是使用getCause方法從InvocationTargetException中提取的。

若是嘗試經過反射調用測試方法會拋出除InvocationTargetException以外的任何異常,則表示編譯時未捕獲到沒有使用的Test註解。 這些用法包括註解實例方法,具備一個或多個參數的方法或不可訪問的方法。 測試運行器中的第二個catch塊會捕獲這些Test使用錯誤並顯示相應的錯誤消息。 這是在RunTestsSample上運行時打印的輸出:

public static void Sample.m3() failed: RuntimeException: Boom

Invalid @Test: public void Sample.m5()

public static void Sample.m7() failed: RuntimeException: Crash

Passed: 1, Failed: 3

如今,讓咱們添加對僅在拋出特定異常時才成功的測試的支持。 咱們須要爲此添加一個新的註解類型:

// Annotation type with a parameter

import java.lang.annotation.*;

/**

 * Indicates that the annotated method is a test method that

 * must throw the designated exception to succeed.

 */

@Retention(RetentionPolicy.RUNTIME)

@Target(ElementType.METHOD)

public @interface ExceptionTest {

    Class<? extends Throwable> value();

}

此註解的參數類型是Class<? extends Throwable>。毫無疑問,這種通配符是拗口的。 在英文中,它表示「擴展Throwable的某個類的Class對象」,它容許註解的用戶指定任何異常(或錯誤)類型。 這個用法是一個限定類型標記的例子(條目 33)。 如下是註解在實踐中的例子。 請注意,類名字被用做註解參數值:

// Program containing annotations with a parameter

public class Sample2 {

    @ExceptionTest(ArithmeticException.class)

    public static void m1() {  // Test should pass

        int i = 0;

        i = i / i;

    }

    @ExceptionTest(ArithmeticException.class)

    public static void m2() {  // Should fail (wrong exception)

        int[] a = new int[0];

        int i = a[1];

    }

    @ExceptionTest(ArithmeticException.class)

    public static void m3() { }  // Should fail (no exception)

}

如今讓咱們修改測試運行器工具來處理新的註解。 這樣將包括將如下代碼添加到買呢方法中:

if (m.isAnnotationPresent(ExceptionTest.class)) {

    tests++;

    try {

        m.invoke(null);

        System.out.printf("Test %s failed: no exception%n", m);

    } catch (InvocationTargetException wrappedEx) {

        Throwable exc = wrappedEx.getCause();

        Class<? extends Throwable> excType =

            m.getAnnotation(ExceptionTest.class).value();

        if (excType.isInstance(exc)) {

            passed++;

        } else {

            System.out.printf(

                "Test %s failed: expected %s, got %s%n",

                m, excType.getName(), exc);

        }

    } catch (Exception exc) {

        System.out.println("Invalid @Test: " + m);

    }

}

此代碼與咱們用於處理Test註解的代碼相似,只有一個例外:此代碼提取註解參數的值並使用它來檢查測試引起的異常是否屬於正確的類型。 沒有明確的轉換,所以沒有ClassCastException的危險。 測試程序編譯的事實保證其註解參數表明有效的異常類型,但有一點須要注意:若是註解參數在編譯時有效,但表明指定異常類型的類文件在運行時再也不存在,則測試運行器將拋出TypeNotPresentException異常。

將咱們的異常測試示例進一步推動,能夠設想一個測試,若是它拋出幾個指定的異常中的任何一個,就會經過測試。 註解機制有一個便於支持這種用法的工具。 假設咱們將ExceptionTest註解的參數類型更改成Class對象數組:

// Annotation type with an array parameter

@Retention(RetentionPolicy.RUNTIME)

@Target(ElementType.METHOD)

public @interface ExceptionTest {

    Class<? extends Exception>[] value();

}

註解中數組參數的語法很靈活。 它針對單元素數組進行了優化。 全部之前的ExceptionTest註解仍然適用於ExceptionTest的新數組參數版本,而且會生成單元素數組。 要指定一個多元素數組,請使用花括號將這些元素括起來,並用逗號分隔它們:

// Code containing an annotation with an array parameter

@ExceptionTest({ IndexOutOfBoundsException.class,

                 NullPointerException.class })

public static void doublyBad() {

    List<String> list = new ArrayList<>();



    // The spec permits this method to throw either

    // IndexOutOfBoundsException or NullPointerException

    list.addAll(5, null);

}

修改測試運行器工具以處理新版本的ExceptionTest是至關簡單的。 此代碼替換原始版本:

if (m.isAnnotationPresent(ExceptionTest.class)) {

    tests++;

    try {

        m.invoke(null);

        System.out.printf("Test %s failed: no exception%n", m);

    } catch (Throwable wrappedExc) {

        Throwable exc = wrappedExc.getCause();

        int oldPassed = passed;

        Class<? extends Exception>[] excTypes =

            m.getAnnotation(ExceptionTest.class).value();

        for (Class<? extends Exception> excType : excTypes) {

            if (excType.isInstance(exc)) {

                passed++;

                break;

            }

        }

        if (passed == oldPassed)

            System.out.printf("Test %s failed: %s %n", m, exc);

    }

}

從Java 8開始,還有另外一種方法來執行多值註解。 可使用@Repeatable元註解來標示註解的聲明,而不用使用數組參數聲明註解類型,以指示註解能夠重複應用於單個元素。 該元註解採用單個參數,該參數是包含註解類型的類對象,其惟一參數是註解類型[JLS,9.6.3]的數組。 若是咱們使用ExceptionTest註解採用這種方法,下面是註解的聲明。 請注意,包含註解類型必須使用適當的保留策略和目標進行註解,不然聲明將沒法編譯:

// Repeatable annotation type

@Retention(RetentionPolicy.RUNTIME)

@Target(ElementType.METHOD)

@Repeatable(ExceptionTestContainer.class)

public @interface ExceptionTest {

    Class<? extends Exception> value();

}



@Retention(RetentionPolicy.RUNTIME)

@Target(ElementType.METHOD)

public @interface ExceptionTestContainer {

    ExceptionTest[] value();

}

下面是咱們的doublyBad測試用一個重複的註解代替基於數組值註解的方式:

// Code containing a repeated annotation

@ExceptionTest(IndexOutOfBoundsException.class)

@ExceptionTest(NullPointerException.class)

public static void doublyBad() { ... }

處理可重複的註解須要注意。重複註解會生成包含註解類型的合成註解。 getAnnotationsByType方法掩蓋了這一事實,可用於訪問可重複註解類型和非重複註解。但isAnnotationPresent明確指出重複註解不是註解類型,而是包含註解類型。若是某個元素具備某種類型的重複註解,而且使用isAnnotationPresent方法檢查元素是否具備該類型的註釋,則會發現它沒有。使用此方法檢查註解類型的存在會所以致使程序默默忽略重複的註解。一樣,使用此方法檢查包含的註解類型將致使程序默默忽略不重複的註釋。要使用isAnnotationPresent檢測重複和非重複的註解,須要檢查註解類型及其包含的註解類型。如下是RunTests程序的相關部分在修改成使用ExceptionTest註解的可重複版本時的例子:

// Processing repeatable annotations

if (m.isAnnotationPresent(ExceptionTest.class)

    || m.isAnnotationPresent(ExceptionTestContainer.class)) {

    tests++;

    try {

        m.invoke(null);

        System.out.printf("Test %s failed: no exception%n", m);

    } catch (Throwable wrappedExc) {

        Throwable exc = wrappedExc.getCause();

        int oldPassed = passed;

        ExceptionTest[] excTests =

                m.getAnnotationsByType(ExceptionTest.class);

        for (ExceptionTest excTest : excTests) {

            if (excTest.value().isInstance(exc)) {

                passed++;

                break;

            }

        }

        if (passed == oldPassed)

            System.out.printf("Test %s failed: %s %n", m, exc);

    }

}

添加了可重複的註解以提升源代碼的可讀性,從邏輯上將相同註解類型的多個實例應用於給定程序元素。 若是以爲它們加強了源代碼的可讀性,請使用它們,但請記住,在聲明和處理可重複註解時存在更多的樣板,而且處理可重複的註解很容易出錯。

這個項目中的測試框架只是一個演示,但它清楚地代表了註解相對於命名模式的優越性,並且它僅僅描繪了你能夠用它們作什麼的外觀。 若是編寫的工具要求程序員將信息添加到源代碼中,請定義適當的註解類型。當可使用註解代替時,沒有理由使用命名模式

這就是說,除了特定的開發者(toolsmith)以外,大多數程序員都不須要定義註解類型。 但全部程序員都應該使用Java提供的預約義註解類型(條目40,27)。 另外,請考慮使用IDE或靜態分析工具提供的註解。 這些註解能夠提升這些工具提供的診斷信息的質量。 但請注意,這些註解還沒有標準化,所以若是切換工具或標準出現,可能額外須要作一些工做。

相關文章
相關標籤/搜索