由于前幾章在看的過(guò)程中感覺(jué)用處不是特別大,且大部分與自己的理解相同,因此不做記錄。這一部分主要記錄泛型的概念和應(yīng)用,由于自己對(duì)泛型的理解也不是很好,著重記錄一下~由于泛型內(nèi)容有點(diǎn)多,分成上下兩部分。
相關(guān)概念術(shù)語(yǔ)
首先什么是泛型呢。
聲明中具有一個(gè)或多個(gè)類型參數(shù)的類或接口就是泛型類或泛型接口。例如,
List接口有一個(gè)類型參數(shù)E,用于表示其元素類型。該接口的全名是List<E>(讀作「List of E」),但人們通常簡(jiǎn)稱為 List。泛型類和泛型接口統(tǒng)稱為泛型。
每個(gè)泛型定義了一組參數(shù)化類型,這些參數(shù)化類型包括類名或接口名 + 帶尖括號(hào)的參數(shù)列表。 例如List<String>,List是接口名, String是參數(shù)列表中的元素。最后,每個(gè)泛型都定義了一個(gè)原始類型,它是沒(méi)有任何相關(guān)類型參數(shù)的泛型的名稱。例如List(這種被稱作原始類型)。這種定義主要是為了與之前的泛型代碼兼容。
不要使用原始類型
原始類型 vs 泛型
由于泛型是在Java 1.5才加入的,在1.5之前,java聲明集合的方式如下:
private final Collection stamps = ... ;
在1.5之后,使用這種聲明并調(diào)用,會(huì)出發(fā)警告, 且從集合中取出元素需要進(jìn)行強(qiáng)轉(zhuǎn),這里容易出現(xiàn) ClassCastException
stamps.add(new Coin( ... )); // 觸發(fā) "unchecked call" warning
OtherObject stamp = (OtherObject) stamps.get(0); // 在運(yùn)行時(shí)拋出 ClassCastException
當(dāng)聲明泛型后,編譯器會(huì)對(duì)集合中的元素進(jìn)行檢查,在編譯時(shí)就能報(bào)錯(cuò)。當(dāng)取出元素時(shí),會(huì)觸發(fā)自動(dòng)類型轉(zhuǎn)換。我們希望越早發(fā)現(xiàn)錯(cuò)誤越好,最好在編寫代碼時(shí)發(fā)現(xiàn)問(wèn)題,不希望在最終運(yùn)行時(shí)拋出異常
這里有一個(gè)問(wèn)題, 原始類型List可以放入任意對(duì)象,List<Object>同樣可以放入任意對(duì)象,那么這兩者的區(qū)別在哪里呢?
粗略地說(shuō),前者選擇了不使用泛型系統(tǒng),而后者明確地告訴編譯器它能夠保存任何類型的對(duì)象。雖然可以將
List<String>傳遞給List類型的參數(shù),但不能將其傳遞給類型List<Object>的參數(shù)。泛型有子類型規(guī)則,List<String>是原始類型List的子類型,而不是參數(shù)化類型List<Object>的子類型。因此,如果使用原始類型(如 List),就會(huì)失去類型安全性,但如果使用參數(shù)化類型(如 List<Object>)則不會(huì)。
實(shí)際應(yīng)用
當(dāng)我們想編寫一個(gè)方法,該方法接受兩個(gè)集合并返回它們共有的元素?cái)?shù)量,Set 作為參數(shù),作為新手可能這樣寫
//注意這里使用了原始類型,這樣很危險(xiǎn)
static int numElementsInCommon(Set s1, Set s2) {
int result = 0;
for (Object o1 : s1)
if (s2.contains(o1))
result++;
return result;
書中給出的替代方法是使用無(wú)界通配符類型,即Set<?>代替Set,這樣做的好處是可以保證集合類型的一致性(保證集合內(nèi)元素的類型唯一)
當(dāng)然對(duì)于不應(yīng)該使用原始類型的規(guī)則,有一些小的例外:
-
必須在類字面量中使用原始類型,例如
List.class是合法的,但List<String>.class``List<?>.class是非法的 -
instanceof運(yùn)算符的應(yīng)用。 由于泛型信息在運(yùn)行時(shí)被刪除,因此在不是無(wú)界通配符類型之外的參數(shù)化類型上使用 instanceof 操作符是非法的。
//一旦確定 o 是一個(gè) Set,就必須將其強(qiáng)制轉(zhuǎn)換為通配符類型 Set<?>,而不是原始類型 Set。這是一個(gè)經(jīng)過(guò)檢查的強(qiáng)制類型轉(zhuǎn)換,所以不會(huì)引發(fā)編譯器警告
if (o instanceof Set) { // Raw type
Set<?> s = (Set<?>) o; // Wildcard type
...
}
消除 unchecked 警告
使用泛型編程時(shí),總會(huì)看到許多編譯器警告:
- unchecked cast warnings
- unchecked method invocation warnings
- unchecked parameterized vararg type warnings
- unchecked conversion warnings
我們可以通過(guò)消除以上的unchecked warnings來(lái)保證泛型的安全性
//這樣聲明會(huì)給出unchecked警告
Set<Lark> exaltation = new HashSet();
//一般IDE 或編譯器會(huì)給出如下的修改建議
Set<Lark> exaltation = new HashSet<>();
但是由于歷史原因或其他原因,如果不能消除警告,但是可以證明引發(fā)警告的代碼是類型安全的,那么(并且只有在那時(shí))使用 SuppressWarnings("unchecked") 注解來(lái)抑制警告。@SuppressWarnings("unchecked")可以用于單個(gè)局部變量聲明乃至整個(gè)類,但是總是在盡可能小的范圍上使用 SuppressWarnings 注解, 同時(shí)每次使用 SuppressWarnings("unchecked") 注解時(shí),要添加一條注釋,說(shuō)明這樣做是安全的。
總之,unchecked 警告很重要。不要忽視他們。每個(gè) unchecked 警告都代表了在運(yùn)行時(shí)發(fā)生 ClassCastException 的可能性。盡最大努力消除這些警告。如果不能消除 unchecked 警告,并且可以證明引發(fā)該警告的代碼是類型安全的,那么可以在盡可能狹窄的范圍內(nèi)使用 @SuppressWarnings("unchecked") 注釋來(lái)禁止警告。在注釋中記錄你決定隱藏警告的理由。
List 使用優(yōu)于數(shù)組
數(shù)組與泛型的區(qū)別:
- 數(shù)組是協(xié)變的
- 數(shù)組是具體化的
由于這兩個(gè)差異,數(shù)組和泛型不能很好地混合。例如,創(chuàng)建泛型、參數(shù)化類型或類型參數(shù)的數(shù)組是非法的。 即new List<E>[]、new List<String>[]、new E[]均不合法。
數(shù)組是協(xié)變的
如果type2 是 type1的子類型,那么下面一段代碼是合法的
// Long 是 object的子類型,代碼合法,但是會(huì)在運(yùn)行時(shí)拋出異常
Object[] objectArray = new Long[1];
objectArray[0] = "I don't fit in"; // Throws ArrayStoreException
對(duì)于List 則沒(méi)有上述問(wèn)題
//編譯時(shí)報(bào)錯(cuò)
List<Object> ol = new ArrayList<Long>(); // Incompatible types
ol.add("I don't fit in");
數(shù)組是具體化的
數(shù)組在運(yùn)行時(shí)知道并強(qiáng)制執(zhí)行他們的元素類型,如果試圖將 String 元素放入一個(gè) Long 類型的數(shù)組中,就在運(yùn)行時(shí)會(huì)得到 ArrayStoreException;泛型是通過(guò)擦除來(lái)實(shí)現(xiàn)的,只在編譯時(shí)進(jìn)行約束,運(yùn)行時(shí)丟棄(或擦除)元素類型信息。
當(dāng)在轉(zhuǎn)換為數(shù)組類型時(shí)遇到泛型數(shù)組創(chuàng)建錯(cuò)誤或 unchecked 強(qiáng)制轉(zhuǎn)換警告時(shí),通常最好的解決方案是使用集合類型 List<E>,而不是數(shù)組類型 E[]。你可能會(huì)犧牲一些簡(jiǎn)潔性或性能,但作為交換,你可以獲得更好的類型安全性和互操作性。
例如,假設(shè)你希望編寫一個(gè) Chooser 類,該類的構(gòu)造函數(shù)接受一個(gè)集合,而單個(gè)方法返回隨機(jī)選擇的集合元素。
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)];
}
}
但是這樣會(huì)出現(xiàn)一個(gè)問(wèn)題,每次調(diào)用choose()后,都需要將結(jié)果進(jìn)行一次強(qiáng)制轉(zhuǎn)換,且很容易在運(yùn)行時(shí)拋出異常。因此這里改為使用泛型
public class Chooser<T> {
private final T[] choiceArray;
public Chooser(Collection<T> choices) {
//此處會(huì)編譯報(bào)錯(cuò)
choiceArray = choices.toArray();
}
public Object choose() {
Random rnd = ThreadLocalRandom.current();
return choiceArray[rnd.nextInt(choiceArray.length)];
}
}
然而在編譯時(shí)報(bào)錯(cuò)
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
到這也許會(huì)簡(jiǎn)單粗暴的把choiceArray = choices.toArray(); 變?yōu)?choiceArray = (T[]) choices.toArray();,進(jìn)行強(qiáng)制轉(zhuǎn)換。雖然這樣解決了錯(cuò)誤,但是又會(huì)派生一個(gè)警告:
# 強(qiáng)轉(zhuǎn)可能會(huì)出現(xiàn)問(wèn)題,不能保證在運(yùn)行時(shí)轉(zhuǎn)換的安全性,因?yàn)槌绦虿恢李愋?T 代表什么
[unchecked] unchecked cast choiceArray = (T[]) choices.toArray();
^ required: T[], found: Object[]
注意: 雖然Chooser使用了泛型,在運(yùn)行時(shí)元素類型會(huì)被抹去,但這樣是仍然不安全的。
最終無(wú)錯(cuò)誤無(wú)警告的版本:
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()));
}
}
總之?dāng)?shù)組和泛型有非常不同的類型規(guī)則。數(shù)組是協(xié)變的、具體化的;泛型是不變的和可被擦除的。因此,數(shù)組提供了運(yùn)行時(shí)類型安全,而不是編譯時(shí)類型安全,對(duì)于泛型反之亦然。一般來(lái)說(shuō),數(shù)組和泛型不能很好地混合。如果你發(fā)現(xiàn)將它們混合在一起并得到編譯時(shí)錯(cuò)誤或警告,那么你的第一個(gè)反應(yīng)該是將數(shù)組替換為 list。