1. 程式人生 > >效率程式設計 之「列舉和註解」

效率程式設計 之「列舉和註解」

溫馨提示:本系列博文(含示例程式碼)已經同步到 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 的那些事兒 <- 目錄 —— ☆☆☆ ————