泛型
第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 behaviorpublic 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 fieldpublic 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 interfacepublic interface Operation { double apply(double x, double y);}// Emulated extensible enum using an interface - Basic implementationpublic 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 enumpublic 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 declarationimport 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 annotationspublic 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 parameterimport 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 codeif (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介面就是有限制的介面,這種標記介面可以描述整個對象的某個約束條件,或者表明執行個體能夠利用其他某個類的方法進行處理
標記註解勝過標記介面的優點在於,它可以通過預設的方式添加一個或者多個註解類型元素,給已被使用的註解類型添加更多的資訊
標記註解的另一個優點在於,它們是更大的註解機制的一部分
如果標記是應用到任何程式元素而不是類或者介面,就必須使用註解;如果標記只應用給類和介面,就應該優先使用標記介面