《Effective Java》——學習筆記(泛型&枚舉和註解)

泛型

第23條:請不要在新代碼中使用原生態類型

如果使用原生態類型,就失掉了泛型在安全性和表述性方面的所有優勢,比如不應該使用像List這樣的原生態類型,而應該使用如List<Object>、List<String>這樣的類型

第24條:消除非受檢警告

用泛型編程時,會遇到許多編譯器警告:非受檢強制轉化警告、非受檢方法調用警告、非受檢普通數組創建警告,以及非受檢轉換警告。如:

Set<Lark> exaltation = new HashSet();

=>

Set<Lark> exaltation = new HashSet<Lark>(); 

要儘可能地消除每一個非受檢警告,如果消除了所有警告,就可以確保代碼是類型安全的,意味着不會在運行時出現ClassCastException異常

如果無法消除警告,同時可以證明引起警告的代碼是類型安全,(只有在這種情況下才)可以用一個@SuppressWarnings(“unchecked”)註解來禁止這條警告

SuppressWarnings註解可以用在任何粒度的級別中,從單獨的局部變量聲明到整個類都可以,應該始終在儘可能小的範圍中使用SuppressWarnings註解。永遠不要在整個類上使用SuppressWarnings,這麼做可能會掩蓋了重要的警告

每當使用@SuppressWarnings(“unchecked”)註解時,都要添加一條註釋,說明爲什麼這麼做是安全的

第25條:列表優先於數組

數組與泛型相比,有兩個重要的不同點。首先,數組是協變的,泛型是不可變的,如下例:

這段代碼是合法的

// Fails at runtime!
Object[] objectArray = new Long[1];
objectArray[0] = "I donot fit in";

但下面這段代碼則不合法

// Wonot compile!
List<Object> o1 = new ArrayList<Long>();
o1.add("I donot fit in");

數組與泛型之間的第二大區別在於,數組是具體化的,因此數組會在運行時才知道並檢查它們的元素類型約束。相比之下,泛型則是通過擦除來實現的,因此泛型只在編譯時強化它們的類型信息,並在運行時丟棄(或者擦除)它們的元素類型信息

第26條:優先考慮泛型

使用泛型比使用需要在客戶端中進行轉換的類型來得更加安全,也更加容易,在設計新類型的時候,要確保它們不需要這種轉換就可以使用,這通常意味着要把類做成是泛型的,只要時間允許,就把現有的類型都泛型化。這對於這些類型的新用戶來說會變得更加輕鬆,又不會破壞現有的客戶端

第27條:優先考慮泛型方法

編寫泛型方法與編寫泛型類型相類似,聲明類型參數的類型參數列表,處在方法的修飾符及其返回類型之間,如下例,類型參數列表爲<E>,返回類型爲Set<E>

public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
    Set<E> result = new HashSet<E>(s1);
    result.addAll(s2);
    return result;
}

泛型方法的一個顯著特性是,無需明確指定類型參數的值,編譯器通過檢查方法參數的類型來計算類型參數的值

第28條:利用有限制通配符來提升API的靈活性

Java提供了一種特殊的參數化類型,稱作有限制的通配符類型,<? Extends E>稱爲“E的某個子類型”,<? super E>稱爲“E的某種超類”

爲了獲得最大限度的靈活性,要在表示生產者或者消費者的輸入參數上使用通配符類型,如果參數化類型表示一個T生成者,就使用<? extends T>;如果它表示一個T消費者,就使用<? super T>,可以通過PECS記憶,producer-extends,consumer-super,如下例:

List<Apple> apples = new ArrayList<Apple>();
List<? extends Fruit> fruits = apples;
// 此時只能從fruits中取出數據,而不能再add數據,因爲fruits中的數據類型是Fruit的子類,但是具體是什麼類型並不知道,所以fruits.add(...);操作是非法的,而Fruit fruit = fruits.get(0);是合法的
// <? extends T> 只能取(產生)數據,所以是生成者(producer)

List<Fruit> fruits = new ArrayList<Fruit>();
List<? super Apple> = fruits;
// 此時fruits中的數據類型是Apple的超類,但是具體是什麼類型並不知道,所以取出的數據類型只能是Object類型,而fruits.add(new Apple());fruits.add(new GreenApple());操作是合法的

// <? super T>只能存(消費)數據,所以是消費者(consumer)

JDK 8 中的 Collections.copy()源碼如下:

/**
 * Copies all of the elements from one list into another.  After the
 * operation, the index of each copied element in the destination list
 * will be identical to its index in the source list.  The destination
 * list must be at least as long as the source list.  If it is longer, the
 * remaining elements in the destination list are unaffected. <p>
 *
 * This method runs in linear time.
 *
 * @param  <T> the class of the objects in the lists
 * @param  dest The destination list.
 * @param  src The source list.
 * @throws IndexOutOfBoundsException if the destination list is too small
 *         to contain the entire source List.
 * @throws UnsupportedOperationException if the destination list's
 *         list-iterator does not support the <tt>set</tt> operation.
 */
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    int srcSize = src.size();
    if (srcSize > dest.size())
        throw new IndexOutOfBoundsException("Source does not fit in dest");

    if (srcSize < COPY_THRESHOLD ||
        (src instanceof RandomAccess && dest instanceof RandomAccess)) {
        for (int i=0; i<srcSize; i++)
            dest.set(i, src.get(i));
    } else {
        ListIterator<? super T> di=dest.listIterator();
        ListIterator<? extends T> si=src.listIterator();
        for (int i=0; i<srcSize; i++) {
            di.next();
            di.set(si.next());
        }
    }
}

一般來說,如果類型參數只在方法聲明中出現一次,就可以用通配符取代它,如果是無限制的類型參數,就用無限制的通配符取代它;如果是有限制的類型參數,就用有限制的通配符取代它

第29條:優先考慮類型安全的異構容器

將鍵(key)進行參數化而不是將容器參數化,然後將參數化的鍵提交給容器,來插入或者獲取值,用泛型系統來確保值的類型與它的鍵相符

如下例:

public class Favorites {
    private Map<Class<?>, Object> favorites = 
            new HashMap<Class<?>, Object>();

    public <T> void putFavorite(Class<T> type, T instance) {
        if(type == null) {
            throw new NullPointerException("Type is null");
        }
        favorites.put(type, instance);
    }

    public <T> T getFavorite(Class<T> type) {
        return type.cast(favorites.get(type));
    }
}

public static void main(String[] args) {
    Favorites f = new Favorites();
    f.putFavorite(String.class, "Java");
    f.putFavorite(Class.class, Favorites.class);
    String favoriteStr = f.getFavorite(String.class);
    Class favoriteClass = f.getFavorite(Class.class);
}

Favorites實例是異構的:不像普通的map,它的所有鍵都是不同類型的,因此,稱Favorites爲類型安全的異構容器

枚舉和註解

第30條:用enum代替int常量

枚舉類型是指由一組固定的常量組成合法值的類型,在編程語言還沒有引入枚舉類型之前,表示枚舉類型的常用模式是聲明一組具名的int常量,每個類型成員一個常量:

public static final int APPLE_FUJI = 0;

這種方法稱作int枚舉模式,存在着諸多不足,因爲int枚舉是編譯時常量,被編譯到使用它們的客戶端中,如果與枚舉常量關聯的int發生了變化,客戶端就必須重新編譯。如果沒有重新編譯,程序還是可以運行,但是它們的行爲就是不確定

public static final String MQ_TOPIC_VQ_AUDIO_PROCESS = "TOPIC_VQ_AUDIO_PROCESS";

上面這種模式叫做String枚舉模式,會有性能問題,因爲它依賴於字符串的比較操作,還會導致初級用戶把字符串常量硬編碼到客戶端代碼中,而不是使用恰當的域名

從Java1.5開始,提供了另一種可以替代的解決方法,可以避免int和String枚舉模式的缺點,並提供許多額外的好處

public enum Apple { FUJI, PRIPPIN, GRANNY_SMITH };

Java的枚舉本質是int值,就是通過公有的靜態final域爲每個枚舉常量導出實例的類,因爲沒有可以訪問的構造器,枚舉類型是真正的final

枚舉提供了編譯時的類型安全,如果聲明一個參數的類型爲Apple,就可以保證,被傳到該參數上的任何非null的對象引用一定屬於三個有效的Apple值之一。試圖傳遞類型錯誤的值時,會導致編譯時錯誤

包含同名常量的多個枚舉類型可以在一個系統中和平共處,因爲每個類型都有自己的命名空間。可以增加或者重新排列枚舉類型中的常量,而無需重新編譯它的客戶端代碼,因爲導出常量的域在枚舉類型和它的客戶端之間提供了一個隔離層:常量值並沒有被編譯到客戶端代碼中,而是在int枚舉模式之中

枚舉類型還允許添加任意的方法和域,並實現任意的接口,它們提供了所有Object方法的高級實現,實現了Comparable和Serializable接口,並針對枚舉類型的可任意改變性設計了序列化方式

示例如下:

// Enum type with data and behavior
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這樣的枚舉類型並不難,爲了將數據與枚舉常量關聯起來,得聲明實例域,並編寫一個帶有數據並將數據保存在域中的構造器

如果一個枚舉具有普遍適用性,它就應該成爲一個頂層類,如果它只是被用在一個特定的頂層類,它就應該成爲該頂層類的一個成員類

將不同的行爲與每個枚舉常量關聯起來,可以在枚舉類型中聲明一個抽象的apply方法,並在特定於常量的類主體中,用具體的方法覆蓋每個常量的抽象apply方法,這種方法被稱作特定於常量的方法實現

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);

    // Implementing a fromString method on an enum type
    private static final Map<String, Operation> stringToEnum
        = new HashMap<String, Operation>();
    static { // Initialize map from constant name to enum constant
        for (Operation op : values())
            stringToEnum.put(op.toString(), op);
    }
    // Returns Operation for string, or null if string is invalid
    public static Operation fromString(String symbol) {
        return stringToEnum.get(symbol);
    }


    // Test program to perform all operations on given operands
    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));
    }
}

枚舉類型有一個自動產生的valueOf(String)方法,它將常量的名字轉變成常量本身,如果在枚舉類型中覆蓋toString,要考慮編寫一個fromString方法,將定製的字符串表示法變回相應的枚舉

總之,與int常量相比,枚舉要易讀得多,也更加安全,功能更加強大

第31條:用實例域代替序數

永遠不要根據枚舉的序數導出與它關聯的值,而是要將它保存在一個實例域中:

// Enum with integer data stored in an instance field
public enum Ensemble {
    SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5),
    SEXTET(6), SEPTET(7), OCTET(8), DOUBLE_QUARTET(8),
    NONET(9), DECTET(10), TRIPLE_QUARTET(12);

    private final int numberOfMusicians;
    Ensemble(int size) { this.numberOfMusicians = size; }
    public int numberOfMusicians() { return numberOfMusicians; }
}

第32條:用EnumSet代替位域

java.util包提供了EnumSet類來有效地表示從單個枚舉類型中提取的多個值的多個集合,這個類實現Set接口,提供了豐富的功能、類型安全性,以及可以從任何其他Set實現中得到的互用性

public class Text {
    public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH }

    // Any Set could be passed in, but EnumSet is clearly best
    public void applyStyles(Set<Style> styles) {
        // Body goes here
    }

    // Sample use
    public static void main(String[] args) {
        Text text = new Text();
        text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));
    }
}

第33條:用EnumMap代替序數索引

有一種非常快速的Map實現專門用於枚舉鍵,稱作java.util.EnumMap

public class Herb{
    public enum Type {ANNUAL, PERENNIAL, BIENNIAL};

    private final String name;
    private final Type type;

    Herb(String name, Type type){
        this.name = name;
        this.type = type;
    }

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

    public static void main(String[] args) {
        Herb[] garden = {
            new Herb("Basil",    Type.ANNUAL),
            new Herb("Carroway", Type.BIENNIAL),
            new Herb("Dill",     Type.ANNUAL),
            new Herb("Lavendar", Type.PERENNIAL),
            new Herb("Parsley",  Type.BIENNIAL),
            new Herb("Rosemary", Type.PERENNIAL)
        };

        // Using an EnumMap to associate data with an enum
        Map<Herb.Type, Set<Herb>> herbsByType =
            new EnumMap<Herb.Type, Set<Herb>>(Herb.Type.class);
        for (Herb.Type t : Herb.Type.values())
            herbsByType.put(t, new HashSet<Herb>());
        for (Herb h : garden)
            herbsByType.get(h.type).add(h);
        System.out.println(herbsByType);
    }
}

第34條:用接口模擬可伸縮的枚舉

雖然無法編寫可擴展的枚舉類型,卻可以通過編寫接口以及實現該接口的基礎枚舉類型,對它進行模擬。這樣允許客戶端編寫自己的枚舉來實現接口,如果API是根據接口編寫的,那麼在可以使用基礎枚舉類型的任何地方,也都可以使用這些枚舉

// Emulated extensible enum using an interface
public interface Operation {
    double apply(double x, double y);
}

// Emulated extensible enum using an interface - Basic implementation
public enum BasicOperation  implements Operation {
    PLUS("+") {
        public double apply(double x, double y) { return x + y; }
    },
    MINUS("-") {
        public double apply(double x, double y) { return x - y; }
    },
    TIMES("*") {
        public double apply(double x, double y) { return x * y; }
    },
    DIVIDE("/") {
        public double apply(double x, double y) { return x / y; }
    };
    private final String symbol;
    BasicOperation(String symbol) {
        this.symbol = symbol;
    }
    @Override public String toString() {
        return symbol;
    }
}

// Emulated extension enum
public enum ExtendedOperation implements Operation {
    EXP("^") {
        public double apply(double x, double y) {
            return Math.pow(x, y);
        }
    },
    REMAINDER("%") {
        public double apply(double x, double y) {
            return x % y;
        }
    };

    private final String symbol;
    ExtendedOperation(String symbol) {
        this.symbol = symbol;
    }
    @Override public String toString() {
        return symbol;
    }

    // Test class to exercise all operations in "extension enum"
    public static void main(String[] args) {
        double x = Double.parseDouble(args[0]);
        double y = Double.parseDouble(args[1]);
        test(ExtendedOperation.class, x, y);

        System.out.println();  // Print a blank line between tests
        test2(Arrays.asList(ExtendedOperation.values()), x, y);
    }

    // test parameter is a bounded type token  (Item 29)
    private static <T extends Enum<T> & Operation> void test(
            Class<T> opSet, double x, double y) {
        for (Operation op : opSet.getEnumConstants())
            System.out.printf("%f %s %f = %f%n",
                              x, op, y, op.apply(x, y));
    }

    // test parameter is a bounded wildcard type (Item 28)
    private static void test2(Collection<? extends Operation> opSet,
                              double x, double y) {
        for (Operation op : opSet)
            System.out.printf("%f %s %f = %f%n",
                              x, op, y, op.apply(x, y));
    }
}

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

定義一個註解類型來指定簡單的測試,它們自動運行,並在拋出異常時失敗

// 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註解類型的聲明就是它自身通過Retention和Target註解進行了註解,註解類型聲明中的這種註解被稱作元註解。@Retention(RetentionPolicy.RUNTIME)元註解表明,Test註解應該在運行時保留,如果沒有保留,測試工具就無法知道Test註解。@Target(ElementType.METHOD)元註解表明,Test註解只在方法聲明中才是合法的:它不能運用到類聲明、域聲明或者其他程序元素上

下面是對Test註解的應用,稱作標記註解,因爲它沒有參數,只是“標註”被註解的元素,如果拼錯Test,或者將Test註解應用到程序元素而非方法聲明,程序就無法編譯:

// 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() { }
}

如上8個方法,只有m1測試會通過,m3和m7會拋出異常,m5是一個實例方法,不屬於註解的有效使用

註解永遠不會改變被註解代碼的語義,但是使它可以通過工具進行特殊的處理

public class RunTests {
    public statis void main(String[] args) throw 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);
    }
}

RunTests通過調用Method.invoke反射式地運行類中所有標註了Test的方法,isAnnotationPresent告知該工具要運行哪些方法。如果測試方法拋出異常,反射機制就會將它封裝在InvocationTargetException中

如果要針對只在拋出特殊異常時才成功的測試添加支持,需要一個新的註解類型:

// Annotation type with an array parameter

import java.lang.annotation.*;

/**
 * Indicates that the annotated method is a test method that
 * must throw the any of the designated exceptions to succeed.
 */

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

使用此註解

// Code containing an annotation with an array parameter
@ExceptionTest({ IndexOutOfBoundsException.class,
            NullPointerException.class })
public static void doublyBad() {
    List<String> list = new ArrayList<String>();

    // The spec permits this method to throw either
    // IndexOutOfBoundsException or NullPointerException
    list.addAll(5, null);
}

修改測試工具來處理新的Exception

// Array ExceptionTest processing code
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);
    }
}

所有的程序員都應該使用Java平臺所提供的預定義的註解類型,還要考慮使用IDE或者靜態分析工具所提供的任何註解,這種註解可以提升由這些工具所提供的診斷信息的質量

第36條:堅持使用Override註解

堅持使用這個註解,可以防止一大類的非法錯誤

IDE具有自動檢查功能,稱作代碼檢驗,如果啓動相應的代碼檢驗功能,當有一個方法沒有Override註解,卻覆蓋了超類方法時,IDE就會產生一條警告

第37條:用標記接口定義類型

標記接口是沒有包含方法聲明的接口,而只是指明(或者標明)一個類實現了具有某種屬性的接口,如Serializable接口,通過實現這個接口,類表明它的實例可以被寫到ObjectOutputStream(被序列化)

標記接口勝過標記註解的一個優點是,標記接口定義的類型是由被標記類的實例實現的;標記註解則沒有定義這樣的類型,這個類型允許在編譯時捕捉在使用標記註解的情況下要到運行時才能捕捉到的錯誤

標記接口勝過標記註解的另一個優點是,它們可以被更加精確地進行鎖定,如果註解類型利用@Target(ElementType.TYPE)聲明,它就可以被應用到任何類或者接口。假設有一個標記只適用於特殊接口的實現,如果將它定義成一個標記接口,就可以用它將唯一的接口擴展成它適用的接口,如Set接口就是有限制的接口,這種標記接口可以描述整個對象的某個約束條件,或者表明實例能夠利用其他某個類的方法進行處理

標記註解勝過標記接口的優點在於,它可以通過默認的方式添加一個或者多個註解類型元素,給已被使用的註解類型添加更多的信息

標記註解的另一個優點在於,它們是更大的註解機制的一部分

如果標記是應用到任何程序元素而不是類或者接口,就必須使用註解;如果標記只應用給類和接口,就應該優先使用標記接口

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