Effective Java 3rd 讀書筆記(二)泛型(上)

由于前幾章在看的過(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ī)則,有一些小的例外:

  1. 必須在類字面量中使用原始類型,例如List.class是合法的,但List<String>.class``List<?>.class是非法的
  2. 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é)變的

如果type2type1的子類型,那么下面一段代碼是合法的

// 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。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容