1. 程式人生 > >Effective Java 3rd 條目28 列表優於佇列

Effective Java 3rd 條目28 列表優於佇列

佇列在兩個方向上與泛型不相同。首先,佇列是協變的。這個聽上去令人不安的單詞意思僅僅是:如果Sub是Super的一個子類,那麼佇列型別Sub[]是Super[]的一個子類。相反,泛型是非協變的:對於任何兩個不同型別Type1和Type2,List<Type1>既不是List<Type2>的一個子類,也不是它的超類[JLS, 4.10; Naftalin07, 2.5]。你可能認為這意味著,泛型是有缺陷的,然而大概是佇列有缺陷的。以下程式碼片段是合法的:

// 執行時失敗!
Object[] objectArray = new Long[1]; 
objectArray[
0] = "I don't fit in"; // 丟擲ArrayStoreException

但是如下不是合法的:

// 編譯不通過!

List<Object> ol = new ArrayList<Long>(); // 不可相容型別 
ol.add("I don't fit in");

兩種方式都不能把String放到Long容器裡面,但是使用佇列,你會在執行時發現一個錯誤;使用列表,你會在編譯時發現一個錯誤。當然,你應該寧願在編譯時發現錯誤。
佇列和泛型之間第二個主要區別是,佇列是具體化的[JLS, 4.7]。這意味著,佇列在執行時知道和約束了它們的元素型別。就像以前提到的,如果你試著把一個String到一個Long佇列中,你將得到ArrayStoreException。相反,泛型是實施為型別擦除的[JLS, 4.6]。這意味著,它們僅僅在編譯時約束了它們的型別,而且在允許時拋棄了(擦除了)它們的元素型別資訊。擦除使得泛型型別和不使用泛型的遺留程式碼自由地互操作(條目26),這保證了在Java5中順利地過度到泛型。

因為這些基本的不同,佇列和泛型不能夠很好地混合使用。例如,建立一個泛型型別的、引數化型別的或者型別引數的佇列是不合法的。所以,這些佇列建立表示式都不是合法的:new List<E>[], new List<String>[], new E[]。所有這些都會導致編譯時泛型佇列建立錯誤。

建立泛型佇列為什麼是不合法的呢?因為它不是型別安全的。如果它是合法的,在其他正確程式中,編譯器產生的強轉可能在執行時以ClassCastException方式失敗了。這違反了泛型型別系統提供的基本保證。

為了使得更加具體,考慮如下程式碼片段:

// 泛型佇列建立為什麼是不合法的 - 編譯不通過! 
List<String>[] stringLists = new List<String>[1]; // (1) List<Integer> intList = List.of(42); // (2) Object[] objects = stringLists; // (3) objects[0] = intList; // (4) String s = stringLists[0].get(0); // (5)

讓我們假設,第一行,建立了泛型佇列,是合法的。第二行建立和初始化了一個List<Integer>,它包含了單一元素。第三行儲存List<String>佇列到Object佇列變數中,這是合法的,因為佇列是協變的。第四行儲存 List<Integer> 到Object佇列的單個元素中,這是可以成功的,因為泛型是實現擦除的:List<Integer>例項的執行型別僅僅是List,而且List<String>[]例項的執行型別是List[],所以這個賦值不會產生ArrayStoreException。現在我們有麻煩了。我們儲存了List<Integer>例項到一個宣告為僅僅儲存List<String>例項的列表中。在第五行,我們從這個佇列的單個列表中取得單個元素。編譯器自動強轉取到的元素到String,但是它是一個Integer,所以,我們在執行時獲得ClassCastException。為了阻止這個發生,第一行(它建立了泛型佇列)必須產生一個編譯時錯誤。

像E, List<E>, and List<String>型別技術上被認為是不合具體化的型別[JLS, 4.7]。直觀上來說,不可具體化型別是這樣的型別,相對於編譯時表示,它的執行時表示包含了更少的資訊。因為擦除,可具體化的唯一引數化型別是像List<?>和Map<?,?>這樣的非受限萬用字元型別(條目26)。建立非受限萬用字元型別的佇列是合法的,雖然極少使用。

禁止泛型佇列建立可能很惱人。這意味著,例如,泛型集合返回一個它的元素型別的佇列,這通常是不可能的(但是為部分解決方案參考條目33)。這也意味著,當使用varargs方法(條目53),與泛型型別結合,你將得到令人困惑的警告。如果這個佇列的元素型別是不可具體化的,那麼你將得到一個警告。SafeVarargs註解可以使用在解決這個問題(條目32)。

當你得到一個泛型佇列建立錯誤或者一個對於佇列型別強轉的非受檢強轉警告,最好的解決方案是經常使用結合型別List<E>,而不是佇列型別E[]。你可能犧牲簡明或者效能,但是作為交換,你獲得更好的型別安全和互操作性。

例如,假設你想要編寫一個具有接受集合構造子和一個返回隨機選擇集合的元素的單個方法的Chooser類 。取決於你傳入到構造子的何種集合,你可能使用chooser作為一個遊戲骰子、魔法8球或者蒙地卡羅模擬器的資料來源。以下是一個沒有泛型的簡單實現:

// Chooser - 一個極其需要泛型的類! 
public class Chooser { 
	private final Object[] choiceArray;

	public Chooser(Collection choices) { 
		choiceArray = choices.toArray(); 
	}

	public Object choose() { 
		Random rnd = ThreadLocalRandom.current(); 
		return choiceArray[rnd.nextInt(choiceArray.length)]; 
	}
}

為了使用這個類,每次使用這個方法的時候,你不得不把choose的返回型別從Object強轉為需要要的型別,而且如果你得到型別錯誤,這個強轉將會在執行時失敗。把條目29的建議記到心裡,我們嘗試著修改Chooser使得它是泛型。改變如下粗體所示:

// A first cut at making Chooser generic - won't compile 
public class Chooser<T> { 
	private final T[] choiceArray;
	public Chooser(Collection<T> choices) { 
		choiceArray = choices.toArray(); 
	}

	// choose method unchanged
}

如果你編譯這個類,你會得到這個錯誤資訊:

Chooser.java:9: error: incompatible types: Object[] cannot be 
converted to T[]
	choiceArray = choices.toArray(); 
								  ^ 
	where T is a type-variable:
	 T extends Object declared in class Chooser

沒多大關係,你可以說,可以把Object佇列強轉為一個T佇列:

choiceArray = (T[]) choices.toArray();

這可以擺脫這個錯誤,但是你反而會有一個警告:

Chooser.java:9: warning: [unchecked] unchecked cast 
		choiceArray = (T[]) choices.toArray(); 
										    ^
	required: T[], found: Object[]
	where T is a type-variable:
  T extends Object declared in class Chooser

編譯器告訴你:你不能保證執行時強轉的安全性,因為這個程式不會知道T型別代表著什麼,基礎,元素型別資訊在執行時會從泛型中擦除。這個程式可以執行嗎?當然,但是編譯器不能證明這個。你可以自己證明,把證明放到註釋之中,而且用註釋取消這個警告,但是你最好消除這個警告的根源(條目27)。

為了消除未受檢的強轉警告,使用列表而不是佇列。這個一個Chooser類的版本,編譯時沒有錯誤或者警告:

// 基於列表的Chooser - 安全型別
public class Chooser<T> { 
	private final List<T> choiceList;

	public Chooser(Collection<T> choices) { 
		choiceList = new ArrayList<>(choices); 
	}

	public T choose() { 
		Random rnd = ThreadLocalRandom.current(); 
		return choiceList.get(rnd.nextInt(choiceList.size())); 
	}
}

這個版本有點囉嗦,而且或許有點慢,但是為了內心能夠平靜,你不會再執行時不會得到ClassCastException,這是值得的。

總之,佇列和泛型有非常不同的型別規則。佇列是協變的和具體化的;泛型是不變的和擦除的。結果是,佇列提供了執行時型別安全而不是編譯時型別安全,對於泛型反之。通常,佇列和泛型混合的不好。如果你發現自己混合了它們,而且在編譯時錯誤或者警告,你的第一反應應該是用列表替換佇列。