国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 學院 > 開發設計 > 正文

Effective Java

2019-11-14 22:51:05
字體:
來源:轉載
供稿:網友
Effective java - 枚舉與注解Enumeration

于Java 1.5增加的enum type...enum type是由一組固定的常量組成的類型,比如四個季節、撲克花色。在出現enum type之前,通常用一組int常量表示枚舉類型。比如這樣:

public static final int APPLE_FUJI = 0;public static final int APPLE_PipPIN = 1;public static final int APPLE_GRANNY_SMITH = 2;public static final int ORANGE_NAVEL = 0;public static final int ORANGE_TEMPLE = 1;public static final int ORANGE_BLOOD = 2;

如果只是想用作枚舉,感覺這樣也沒什么。但如果把上面的蘋果和橘子互作比較,或者寫成....

int i = (APPLE_FUJI - ORANGE_TEMPLE) / APPLE_PIPPIN;

雖合法但詫異,這是在做果汁嗎?

而且,這種常量是compile-time常量,編譯后一切都結束了,使用這個常量的地方都被替換為該常量的值。如果該常量值需要改變,所有使用該常量的代碼都必須重新編譯。更糟糕的情況是,不重新編譯也可以正常運行,只不過會得到無法預測的結果。(ps:我覺得更遭的是有人直接把常量值寫到代碼里...)

另外,比如上面的APPLE_FUJI,我想打印它的名字,不是它的值。不僅如此,我還想打印所有蘋果,我想打印蘋果一共有多少種類。當然,如果想打印也可以,只是相比直接使用enum,無論怎么做都很麻煩。

如果使用enum,比如:

public enum Apple  { FUJI, PIPPIN, GRANNY_SMITH } public enum Orange { NAVEL, TEMPLE, BLOOD }

看起來就是一堆常量,但是enum沒有實例,也沒有可訪問的構造器,無法對其進行擴展。enum本身就是final,所以很多時候也直接用enum實現singleton。enum在編譯時是類型安全的,比如有地方聲明了上面代碼中的Apple類型的參數,那么被傳到該參數的引用肯定是三種蘋果之一。而且enum本身就是一個類型,可以有自己的方法和field,而且可以實現接口。

附上書中太陽系enum,很難想象如果有類似需求時用普通常量來實現。也許我可以聲明一個Planet類,再給它加上field的方法,然后在一個constant類中聲明為final但這樣卻無法保證Planet類僅用作常量,所以還是用enum吧:

public enum Planet {    MERCURY(3.302e+23, 2.439e6), VENUS(4.869e+24, 6.052e6), EARTH(5.975e+24,            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);    PRivate final double mass; // In kilograms    private final double radius; // In meters    private final double surfaceGravity; // In m / s^2    // 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 mass() {        return mass;    }    public double radius() {        return radius;    }    public double surfaceGravity() {        return surfaceGravity;    }    public double surfaceWeight(double mass) {        return mass * surfaceGravity; // F = ma    }}

然后我們就可以這樣使用Planet enum,無論是值還是名字,使用起來都很自然:

public class WeightTable {    public static void main(String[] args) {        double earthWeight = Double.parseDouble(args[0]);        double mass = earthWeight / Planet.EARTH.surfaceGravity();        for (Planet p :  Planet.values())            System.out.printf("Weight on %s is %f%n",p, p.surfaceWeight(mass));    }}

其實像Planet這樣的方式對多數使用枚舉的場景而言足夠了。也就是說每個Planet常量表達的是不同的數據,但也有例外。比如,我們要為enum中的每一個常量賦予不同的行為。下面是書中用enum表達計算的例子:

import java.util.HashMap;import java.util.Map;public enum Operation {    PLUS("+") {        double apply(double x, double y) {            return x + y;        }    },    MINUS("-") {        double apply(double x, double y) {            return x - y;        }    },    TIMES("*") {        double apply(double x, double y) {            return x * y;        }    },    DIVIDE("/") {        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);    private static final Map<String, Operation> stringToEnum = new HashMap<String, Operation>();    static {         for (Operation op : values())            stringToEnum.put(op.toString(), op);    }    public static Operation fromString(String symbol) {        return stringToEnum.get(symbol);    }    public static void main(String[] args) {        double x = Double.parseDouble(args[0]);        double y = Double.parseDouble(args[1]);        for (Operation op : Operation.values())            System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));    }}

對不同的枚舉常量進行switch..case..其實也能表達出我們想要的效果。如果以后增加了新的常量則需要再對應加上一個case,當然,不加也不會有任何提示,然后最壞的情況就是運行時出了問題。如上面的代碼是常量行為的正確使用方法,即constant-specific method implementation。為行為提供一個抽象,并為每一個常量提供一個實現,即一個枚舉常量也是constant-specific class body。采用這種方式時,如果新增一個常量,則必須提供一個方法實現,否則編譯器會給出提示,這就多了一層保障。

遺憾的是,這種方式也有缺陷。比如我們有這樣一個需求,計算某一天的薪水,這個某一天可以是一周中的某一天,也可能是某個節日,比如周一到周五使用相同的運算方式,周末另算,某節日另算。也就是說我需要在枚舉中聲明代表周一到周日的常量,如果我繼續使用之前的方式去聲明一個抽象方法,如果周一到周五采用完全一樣的計算,則會出現五段完全相同的代碼。但即使這樣我們也不能用回switch..case..方式,增加一個常量時強制選擇其選擇一種行為實現是必須的。于是我們有一種叫strategy enum的方式,即枚舉中聲明另外一個枚舉的field,該field則代表策略,并提供策略相關的行為。下面是書中代碼:

enum PayrollDay {    MONDAY(PayType.WEEKDAY), TUESDAY(PayType.WEEKDAY), WEDNESDAY(            PayType.WEEKDAY), THURSDAY(PayType.WEEKDAY), FRIDAY(PayType.WEEKDAY), SATURDAY(            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 {            double overtimePay(double hours, double payRate) {                return hours <= HOURS_PER_SHIFT ? 0 : (hours - HOURS_PER_SHIFT)                        * payRate / 2;            }        },        WEEKEND {            double overtimePay(double hours, double payRate) {                return hours * payRate / 2;            }        };        private static final int HOURS_PER_SHIFT = 8;        abstract double overtimePay(double hrs, double payRate);        double pay(double hoursWorked, double payRate) {            double basePay = hoursWorked * payRate;            return basePay + overtimePay(hoursWorked, payRate);        }    }}

Annotation

在Java 1.5之前時常有這樣的情況,通過為程序元素進行特殊的命名以提供特殊的功能,比如JUnit中測試方法必須為test開頭。當然,這種方式在某種程度上確實可行,但不夠優雅。比如:

  • 錯誤的文字拼寫并不會有任何提示,直到運行時才會發現出了問題。
  • 其次,這種方式無法特指某個程序元素,比如用戶將某個類名的開頭做了特殊命名,希望作用于類中所有的方法,結果可能沒有提示、沒有效果、沒有意義。
  • 而且,這種方式太單調,比如我想和某個方法的參數或者和聲明拋出的異常進行交互。當然,反射也可以,但問題是我如何在不知道用戶行為的情況下提供反射方法。

平時工作很少提供過注解,大多數情況都是使用別人提供的注解。沒想過沒有注解會是什么樣子,但和naming pattern一比較發現確實太重要了。比如在下面的例子,聲明一個注解用于表示測試方法:

import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)public @interface Test {    //..}

對于代碼中的retention和target,我們有專門的術語叫做"元注解(meta-annotation)"。而對于這種沒有參數,僅僅標注程序元素的注解,我們稱作"標記注解(marker annotation)"。

如果需要給注解聲明參數并不復雜,只是相當于給一個類添加實例field。如下代碼,表示測試時發生異常數組中的異常時進行通過:

import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)public @interface ExceptionTest {    Class<? extends Exception>[] value();}

當然,注解本身對程序元素并沒有直接的影響,它無法改變代碼本身的語義。我們需要依賴于特定的注解處理類。當然,并不是一個注解就對應一個處理類,一個處理類也可以處理很多種注解。比如下面的代碼為Test和ExceptionTest提供了處理:

import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Method;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);                }            }            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();                    Class<? extends Exception>[] excTypes = m.getAnnotation(                            ExceptionTest.class).value();                    int oldPassed = passed;                    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);                }            }        }        System.out.printf("Passed: %d, Failed: %d%n", passed, tests - passed);    }}

代碼就不多做解釋了,主要是通過反射判斷注解和獲取異常。

其實標記注解非常常見,但說到標記注解就不得不說標記接口,比如Serializable什么的僅僅是作為注明。相比接口只能在類名后面加上implements,注解可以作用于更多的程序元素。于是便得出結論,標記接口可以淘汰了?但這樣過于片面。

首先,被接口標記的類提供該接口的實現,而這一點是注解無法做到的,就算有處理類進行補助也無法成為一種約束。就Serializable而言,如果被標記的類沒有提供實現,ObjectOutputStream.write(Object)則毫無意義。另外,這個接口有點特殊,它確實是一種約束,但在編譯期沒給出警告。我之前以為write方法沒有定義在Serializable中可能有什么特殊意義,但作者原話是:

Inexplicably, the authors of the ObjectOutputStream API did not take advantage of the Serializable interface in declaring the write method.

可見他也不知道其中的意義,既然如此,我們也不仿效這種作法了吧。

第二點是接口標記地更加精確。乍一看似乎有些矛盾,相比接口只能作用于類元素,注解可以作用于多種元素不是注解的優點嗎?其實作者表達的并不是這個觀點,就一個接口和Target為ElementType.Type的注解而言,后者可以作用于任何類和接口。

作者用Set接口進行了說明,Set這種情況有些特殊,Set繼承了Collection接口。乍一看,Set似乎不是一個標記接口,它聲明了太多方法。參考:

The Set interface places additional stipulations, beyond those inherited from the Collection interface, on the contracts of all constructors and on the contracts of the add, equals and hashCode methods. Declarations for other inherited methods are also included here for convenience. (The specifications accompanying these declarations have been tailored to the Set interface, but they do not contain any additional stipulations.)

但作者將其描述為"a restricted marker interface",它聲明的方法與Collection接口是相同的。Set并沒有改進Collection的契約,只是為實現類多提供了一種抽象描述。

但即便如此,也不能把注解設計成至少有一個參數的形式。首先不得不承認,能標記的類型比接口更多,這個確實是一個優勢。另外,在一個類中,同一種標記注解可以出現多次,這一點也是其優勢。而最重要的,相比接口這種約定(即,聲明后被一些類提供了實現,在后期版本中很難修改這個接口),注解則可以在后期變得更豐富。


發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
主站蜘蛛池模板: 吉木乃县| 广汉市| 中山市| 沁源县| 宁阳县| 五指山市| 利津县| 珲春市| 栾城县| 咸阳市| 昌吉市| 泰来县| 栾川县| 张家川| 安平县| 剑阁县| 沙雅县| 交城县| 达拉特旗| 龙口市| 原平市| 班玛县| 洛宁县| 屯门区| 思南县| 枣强县| 友谊县| 巨野县| 日土县| 牡丹江市| 福清市| 荣昌县| 肃南| 自贡市| 武夷山市| 海伦市| 封丘县| 保亭| 外汇| 松原市| 苍南县|