天天看點

Java泛型的重要目的:别讓貓别站在狗隊裡

《Java程式設計思想》第四版足足用了75頁來講泛型——厚厚的一沓内容,很容易讓人頭大——但其實根本不用這麼多,隻需要一句話:我是一個泛型隊列,狗可以站進來,貓也可以站進來,但最好不要既站貓,又站狗!

01、泛型是什麼

泛型,有人拆解這個詞為“參數化類型”。這種拆解其實也不好了解,還是按照沉默王二的意思來了解一下吧。

現在有一隻玻璃杯,你可以讓它盛一杯白開水,也可以盛一杯二鍋頭——泛型的概念就在于此,制造這隻杯子的時候沒必要在說明書上定義死,指明它隻能盛白開水而不能盛二鍋頭!

可以在說明書上指明它用來盛裝液體,但最好也不要這樣,弄不好使用者想用它來盛幾塊冰糖呢!

這麼一說,你是不是感覺不那麼抽象了?泛型其實就是在定義類、接口、方法的時候不局限地指定某一種特定類型,而讓類、接口、方法的調用者來決定具體使用哪一種類型的參數。

就好比,玻璃杯的制造者說,我不知道使用者用這隻玻璃杯來幹嘛,是以我隻負責造這麼一隻杯子;玻璃杯的使用者說,這就對了,我來決定這隻玻璃杯是盛白開水還是二鍋頭,或者冰糖。

02、什麼時候用泛型

我們來看一段簡短的代碼:

public class Cmower {

    class Dog {
    }

    class Cat {
    }

    public static void main(String[] args) {
        Cmower cmower = new Cmower();
        Map map = new HashMap();
        map.put("dog", cmower.new Dog());
        map.put("cat", cmower.new Cat());

        Cat cat = (Cat) map.get("dog");
        System.out.println(cat);
    }

}
           

這段代碼的意思是:我們在map中放了一隻狗(Dog),又放了一隻貓(Cat),當我們想從map中取出貓的時候,卻一不留神把狗取了出來。

這段代碼編譯是沒有問題的,但運作的時候就會報

ClassCastException

(狗畢竟不是貓啊):

Exception in thread "main" java.lang.ClassCastException: com.cmower.java_demo.sixteen.Cmower$Dog cannot be cast to com.cmower.java_demo.sixteen.Cmower$Cat
    at com.cmower.java_demo.sixteen.Cmower.main(Cmower.java:20)
           

為什麼會這樣呢?

1)寫代碼的程式員粗心大意。要從map中把貓取出來,你不能取狗啊!

2)建立map的時候,沒有明确指定map中要放的類型。如果指定是要放貓,那肯定取的時候就是貓,不會取出來狗;如果指定是要放狗,也一個道理。

第一種情況不太好解決,總不能把程式員打一頓(我可不想做一個天天背鍋的程式員,很重的好不好);第二種情況就比較容易解決,因為Map支援泛型(泛型接口)。

public interface Map<K,V> {
}
           

注:在Java中,經常用T、E、K、V等形式的參數來表示泛型參數。

T:代表一般的任何類。

E:代表 Element 的意思,或者 Exception 異常的意思。

K:代表 Key 的意思。

V:代表 Value 的意思,通常與 K 一起配合使用。

既然Map支援泛型,那作為Map的實作者HashMap(泛型類)也支援泛型了。

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {

}
           

其中的put方法(泛型方法)是這樣定義的:

public V put(K key, V value) {
  return putVal(hash(key), key, value, false, true);
}
           

好了,現在使用泛型的形式來定義一個隻能放Cat的Map吧!

public class Cmower {

    class Dog {
    }

    class Cat {
    }

    public static void main(String[] args) {
        Cmower cmower = new Cmower();
        Map<String, Cat> map = new HashMap<>();
//        map.put("dog", cmower.new Dog()); // 不再允許添加
        map.put("cat", cmower.new Cat());

        Cat cat = map.get("cat");
        System.out.println(cat);
    }
}
           

當使用泛型定義map(鍵為String類型,值為Cat類型)後:

1)編譯器就不再允許你向map中添加狗的對象了。

2)當你從map中取出貓的時候,也不再需要強制轉型了。

03、類型擦除

有人說,Java的泛型做的隻是表面功夫——泛型資訊存在于編譯階段(狗隊在編譯時不允許站貓),運作階段就消失了(運作時的隊列裡沒有貓的資訊,連狗的資訊也沒有)——這種現象被稱為“類型擦除”。

來,看代碼解釋一下:

public class Cmower {

    class Dog {
    }

    class Cat {
    }

    public static void main(String[] args) {
        Cmower cmower = new Cmower();
        Map<String, Cat> map = new HashMap<>();
        Map<String, Dog> map1 = new HashMap<>();

        // The method put(String, Cmower.Cat) in the type Map<String,Cmower.Cat> is not applicable for the arguments (String, Cmower.Dog)
        //map.put("dog",cmower.new Dog());

        System.out.println(map.getClass());
        // 輸出:class java.util.HashMap
        System.out.println(map1.getClass());
        // 輸出:class java.util.HashMap
    }

}
           

map的鍵位上是Cat,是以不允許put一隻Dog;否則編譯器會提醒

The method put(String, Cmower.Cat) in the type Map<String,Cmower.Cat> is not applicable for the arguments (String, Cmower.Dog)

。編譯器做得不錯,值得點贊。

但是問題就來了,map的Class類型為HashMap,map1的Class類型也為HashMap——也就是說,Java代碼在運作的時候并不知道map的鍵位上放的是Cat,map1的鍵位上放的是Dog。

那麼,試着想一些可怕的事情:既然運作時泛型的資訊被擦除了,而反射機制是在運作時确定類型資訊的,那麼利用反射機制,是不是就能夠在鍵位為Cat的Map上放一隻Dog呢?

我們不妨來試一試:

public class Cmower {

    class Dog {
    }

    class Cat {
    }

    public static void main(String[] args) {
        Cmower cmower = new Cmower();
        Map<String, Cat> map = new HashMap<>();

        try {
            Method method = map.getClass().getDeclaredMethod("put",Object.class, Object.class);

            method.invoke(map,"dog", cmower.new Dog());

            System.out.println(map);
            // {dog=com.cmower.java_demo.sixteen.Cmower$Dog@55f96302}
        } catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
            e.printStackTrace();
        }
    }

}
           

看到沒?我們竟然在鍵位為Cat的Map上放了一隻Dog!

注:Java的設計者在JDK 1.5時才引入了泛型,但為了照顧以前設計上的缺陷,同時相容非泛型的代碼,不得不做出了一個折中的政策:編譯時對泛型要求嚴格,運作時卻把泛型擦除了——要相容以前的版本,還要更新擴充新的功能,真的很不容易!

04、泛型通配符

有些時候,你會見到這樣一些代碼:

List<? extends Number> list = new ArrayList<>();
List<? super Number> list = new ArrayList<>();
           

?

和關鍵字

extends

或者

super

在一起其實就是泛型的進階應用:通配符。

我們來自定義一個泛型類——PetHouse(寵物小屋),它有一些基本的動作(可以住進來一隻寵物,也可以放出去):

public class PetHouse<T> {
    private List<T> list;

    public PetHouse() {
    }

    public void add(T item) {
        list.add(item);
    }

    public T get() {
        return list.get(0);
    }
}
           

如果我們想要住進去一隻寵物,可以這樣定義小屋(其泛型為Pet):

PetHouse<Pet> petHouse = new PetHouse<>();
           

然後,我們讓小貓和小狗住進去:

petHouse.add(new Cat());
petHouse.add(new Dog());
           

如果我們隻想要住進去一隻小貓,打算這樣定義小屋:

PetHouse<Pet> petHouse = new PetHouse<Cat>();
           

但事實上,編譯器不允許我們這樣定義:因為泛型不直接支援向上轉型。該怎麼辦呢?

可以這樣定義小屋:

PetHouse<? extends Pet> petHouse = new PetHouse<Cat>();
           

也就是說,寵物小屋可以住進去小貓,但它必須是寵物(Pet或者Pet的子類)而不是一隻野貓。

但很遺憾,這個寵物小屋實際上住不了小貓,看下圖。

Java泛型的重要目的:别讓貓别站在狗隊裡

這是因為Java雖然支援泛型的向上轉型(使用

extends

通配符),但我們卻無法向其中添加任何東西——編譯器并不知道寵物小屋裡要住的是小貓還是小狗,或者其他寵物,是以幹脆什麼都不讓住。

看到這,你一定非常疑惑,既然

PetHouse<? extends Pet>

定義的寵物小屋什麼也不讓住,那為什麼還要這樣定義呢?思考一下。

05、讀者将軍的總結

泛型限定符有一描述:上界不存下界不取。

上界不存的原因:例如 List,編譯器隻知道容器内是 Father 及其子類,具體是什麼類型并不知道,編譯器在看到 extends 後面的 Father 類,隻是标上一個

CAP#1

作為占位符,無論往裡面插什麼,編譯器都不知道能不能和

CAP#1

比對,是以就不允許插入。

extends的作用:可以在初始化的時候存入一個值,并且能保證資料的穩定性,隻能取不能存。讀取出來的資料可以存在父類或者基類裡。

下界不取的原因:下界限定了元素的最小粒度,實際上是放松了容器元素的類型控制。例如 List, 元素是 Father 的基類,可以存入 Father 及其子類。但編譯器并不知道哪個是 Father 的超類,如 Human。讀取的時候,自然不知道是什麼類型,隻能傳回 Object,這樣元素資訊就全部丢失了。

super的作用:用于參數類型限定。

PECS 原則:

1.頻繁往外讀取内容的,适合用extends

2.經常往裡插入的,适合用super

上一篇:HashMap,難的不在Map,而在Hash

下一篇:Java異常處理:給程式罩一層保險

微信掃描左側二維碼,關注作者的微信公衆号:「

沉默王二

背景回複“

666

”即可擷取一份 500G 的高清教學視訊,并且已經分門别類,可以按需下載下傳,速去!

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接,否則保留追究法律責任的權利。

如果覺得還有幫助的話,可以點一下右下角的【推薦】。