效率編程 之「枚舉和註解」

溫馨提示:本系列博文(含示例代碼)已經同步到 GitHub,地址爲「java-skills」,歡迎感興趣的童鞋StarFork,糾錯。

第 1 條:用enum代替int常量

枚舉類型是指由一組固定的常量組成合法值的類型,例如人的性別、中國的省份名稱等。在 Java 1.5 發行版之前,表示枚舉類的常用模式是聲明一組具名的int常量,每個類型成員一個常量:

public class IntEnum {
    public static final int MAN = 0;
    public static final int WOMAN = 1;
}

上面的方法稱之爲“int枚舉模式”,存在着很多的不足。它在類型安全性和使用方便性方面沒有任何幫助。因爲int枚舉是編譯時常量,被編譯到使用它們的客戶端中,如果與枚舉常量關聯的int值發生了變化,客戶端就必須重新編譯。否則的話,程序可以運行,但運行的行爲就是不確定的。幸運的是,從 Java 1.5 發行版本開始,提供了專門用於表示枚舉的enum類型:

public enum Orange {
    NAVEL,
    TEMOLE,
    BLOOD
}

Java 枚舉類型的本質上是int值,其背後的基本思想非常簡單:它們就是通過公有的靜態final域爲每個枚舉常量導出實例的類。因爲沒有可以訪問的構造器,枚舉類型是真正final的。枚舉還提供了編譯時的安全性。包含同名常量的多個枚舉類型可以在一個系統中和平共處,因爲每個類型都有自己的命名空間。此外,枚舉類型還允許添加任意的方法和域,並實現任意的接口。

public enum Planet {
    MERCURY(3.302e+23, 2.439e6),
    VENUS(4.869e+24, 6.052e6),
    EARTH(5.975e+23, 6.378e6),
    MARS(6.419e+23, 3.393e6),
    JUPITER(1.899e+27, 7.149e7),
    SATURN(5.685e+26, 6.027e7),
    URANUS(8.683e+25, 2.556e7),
    NEPTUNE(1.024e+26, 2.477e7);

    // In kilograms
    private final double mass;
    // In meters
    private final double radius;
    // In m / s^2
    private final double surfaceGravity;

    // Universal gravitational constant in m^3 / kg s^2
    private static final double G = 6.67300E-11;

    // constructor
    Planet(double mass, double radius) {
        this.mass = mass;
        this.radius = radius;
        surfaceGravity = G * mass / (radius * radius);
    }

    public double getMass() {
        return mass;
    }

    public double getRadius() {
        return radius;
    }

    public double getSurfaceGravity() {
        return surfaceGravity;
    }

    public double surfaceWeight(double mass) {
        // F = ma
        return mass * surfaceGravity;
    }
}

編寫一個像Planet這樣的枚舉類型並不難。爲了將數據與枚舉常量關聯起來,得聲明實例域,並編寫一個帶有數據並將數據保存在域中的構造器。枚舉天生就是不可變的,因此所有的域都應該爲final的。它們可以是公有的,但最好將它們做成是私有的,並提供公有的訪問方法。

如果一個枚舉具有普遍適用性,它就應該成爲一個頂層類;如果它只是被用在一個特定的頂層類中,它就應該成爲該頂層類的一個成員類。如果枚舉類型中定義了抽象方法,那麼這個抽象方法就必須被它所有常量中的具體方法所覆蓋。例如,

public enum Operation {
    PLUS("+") {
        @Override
        double apply(double x, double y) {
            return x + y;
        }
    },
    MINUS("-") {
        @Override
        double apply(double x, double y) {
            return x - y;
        }
    },
    TIMES("*") {
        @Override
        double apply(double x, double y) {
            return x * y;
        }
    },
    DIVIDE("/") {
        @Override
        double apply(double x, double y) {
            return x / y;
        }
    };

    private final String symbol;

    Operation(String symbol) {
        this.symbol = symbol;
    }

    @Override
    public String toString() {
        return symbol;
    }

    abstract double apply(double x, double y);
}

枚舉類型有一個自動產生的valueOf(String)方法,它將常量的名字轉成常量本身;還有一個values()方法,可以返回枚舉類型中定義的所有枚舉值。枚舉構造器不可以訪問枚舉的靜態域,除了編譯時常量域之外。這一限制是有必要的,因爲構造器運行的時候,這些靜態域還沒有被初始化。此外,還有一種比較特殊的情況,即在枚舉中設置枚舉,我們稱之爲“策略枚舉”,如:

public enum PayrollDay {
    MONDAY(PayType.WEEKDAY), TUESDAY(PayType.WEEKDAY), WEDNESDAY(PayType.WEEKDAY),
    THURSDAY(PayType.WEEKDAY), FRIDAY(PayType.WEEKDAY), SATURADY(PayType.WEEKEND), SUNDAY(PayType.WEEKEND);

    private final PayType payType;

    PayrollDay(PayType payType) {
        this.payType = payType;
    }

    // 調用策略枚舉中的方法,計算工資
    double pay(double hoursWorked, double payRate) {
        return payType.pay(hoursWorked, payRate);
    }

    // 策略枚舉
    private enum PayType {
        WEEKDAY {
            @Override
            double overtimePay(double hours, double payRate) {
                return hours <= HOURS_PER_SHIFT ? 0 :
                        (hours - HOURS_PER_SHIFT) * payRate / 2;
            }
        },
        WEEKEND {
            @Override
            double overtimePay(double hours, double payRate) {
                return hours * payRate / 2;
            }
        };

        private static final int HOURS_PER_SHIFT = 8;

        // 強制策略枚舉中的每個枚舉都覆蓋此方法
        abstract double overtimePay(double hours, double payRate);

        // 實際計算工資的方法
        double pay(double hoursWorked, double payRate) {
            double basePay = hoursWorked * payRate;
            return basePay + overtimePay(hoursWorked, payRate);
        }
    }
}

如上述代碼所示,我們在實現計算工資(基礎工資 + 超時工資)的情景下,使用了策略枚舉。通過策略枚舉,使我們的代碼更加安全和簡潔。總之,如果多個枚舉常量同時共享相同的行爲,就應該考慮使用策略枚舉。

第 2 條:註解優先於命名模式

在 Java 1.5 發行版之前,一般使用命名模式表明程序元素需要通過某種工具或者框架進行特殊處理。例如,JUnit 測試框架原本要求它的用戶一定要用test作爲測試方法的開頭,這種方法可行,但是有幾個很嚴重的缺點:

  • 文字拼寫錯誤會導致失敗,且沒有任何提示;
  • 無法確保它們只用於相應的程序元素上;
  • 它們沒有提供將參數值與程序元素管理起來的好方法。

不過,註解的出現,很好的解決了所有這些問題。假設想要定義一個註解類型來指定簡單的測試,它們自動運行,並在拋出錯誤時失敗。以下就是這樣的一個註解類型,命名爲Test

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}

Test註解類型的聲明就是它自身通過RetentionTarget註解進行了註解。註解類型中的這種註解被稱作元註解。@Retention(RetentionPolicy.RUNTIME)元註解表明,Test註解應該在運行時保留,如果沒有保留,測試工具就無法知道Test註解;@Target(ElementType.METHOD)元註解表明,Test註解只在方法聲明中才是合法的,它不能運用到類聲明、域聲明或者其他程序元素上。此外,Test註解只能用於無參的靜態方法。註解永遠不會改變被註解代碼的語義,但是使它可以通過工具進行特殊的處理。例如像這種簡單的測試運行類:

public class RunTests {
    /**
     * 該方法爲 靜態無參 的,因此可以通過 @Test 測試
     */
    @Test
    public static void testAnnocation() {
        System.out.println("hello world");
    }

    /**
     * 該方法爲 靜態有參 的,因此不可以通過 @Test 測試
     */
    @Test
    public static void testAnnocation2(String word) {
        System.out.println(word);
    }

    /**
     * 該方法爲 非靜態無參 的,因此不可以通過 @Test 測試
     */
    @Test
    public void testAnnocation3() {
        System.out.println("hello world");
    }

    public static void main(String[] args) throws Exception {
        int tests = 0;
        int passed = 0;
        Class testClass = Class.forName(args[0]);
        for (Method method : testClass.getDeclaredMethods()) {
            // 判斷類中的被 @Test 註解的方法
            if (method.isAnnotationPresent(Test.class)) {
                tests++;
                try {
                    // 通過反射,執行被註解的方法
                    method.invoke(null);
                    passed++;
                } catch (InvocationTargetException warppedExc) {
                    Throwable exc = warppedExc.getCause();
                    System.out.println(method + " failed: " + exc);
                } catch (Exception exc) {
                    System.out.println("Invalid @Test: " + method);
                }
            }
        }
        System.out.printf("Passed: %d, Failed: %d%n", passed, tests - passed);
    }
}

test

如上述代碼及執行結果圖所示,通過使用完全匹配的類名如com.hit.effective.chapter5.annotation.RunTests,並通過調用Method.invoke()反射式地運行類中所有標註了Test的方法。isAnnotationPresent()方法告知該工具要運行哪些方法。如果測試方法拋出異常,反射機制就會將它封裝在InvocationTargetException中。該工具捕捉到了這個異常,並打印失敗報告,包含測試方法拋出的原始異常,這些信息通過getCause()方法從InvocationTargetException中提取出來。如果嘗試通過反射調用測試方法時拋出InvocationTargetException之外的任何異常,表明編譯時沒有捕捉到Test註解的無效用法。

除上述方法之外,我們也可以通過判斷是否拋出某種特定的異常作爲判斷是否通過測試的標準,具體方法可以參考 GitHub 上的「java-skills」項目中的RunExceptionTestsRunMoreExceptionTests兩個註解測試示例。總之,既然有了註解,就完全沒有理由再使用命名模式了。


———— ☆☆☆ —— 返回 -> 那些年,關於 Java 的那些事兒 <- 目錄 —— ☆☆☆ ————

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章