Java泛型的重要目的:別讓貓別站在狗隊裏

《Java編程思想》第四版足足用了75頁來說泛型——厚厚的一沓內容,很容易讓人頭大——但其實根本不用這麼多,只須要一句話:我是一個泛型隊列,狗能夠站進來,貓也能夠站進來,但最好不要既站貓,又站狗!html

0一、泛型是什麼

泛型,有人拆解這個詞爲「參數化類型」。這種拆解其實也很差理解,仍是按照沉默王二的意思來理解一下吧。java

如今有一隻玻璃杯,你可讓它盛一杯白開水,也能夠盛一杯二鍋頭——泛型的概念就在於此,製造這隻杯子的時候不必在說明書上定義死,指明它只能盛白開水而不能盛二鍋頭!程序員

能夠在說明書上指明它用來盛裝液體,但最好也不要這樣,弄很差用戶想用它來盛幾塊冰糖呢!編程

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

就比如,玻璃杯的製造者說,我不知道使用者用這隻玻璃杯來幹嗎,因此我只負責造這麼一隻杯子;玻璃杯的使用者說,這就對了,我來決定這隻玻璃杯是盛白開水仍是二鍋頭,或者冰糖。微信

0二、何時用泛型

咱們來看一段簡短的代碼:app

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中取出貓的時候,卻一不留神把狗取了出來。spa

這段代碼編譯是沒有問題的,但運行的時候就會報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)

爲何會這樣呢?code

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中取出貓的時候,也再也不須要強制轉型了。

0三、類型擦除

有人說,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時才引入了泛型,但爲了照顧之前設計上的缺陷,同時兼容非泛型的代碼,不得不作出了一個折中的策略:編譯時對泛型要求嚴格,運行時卻把泛型擦除了——要兼容之前的版本,還要升級擴展新的功能,真的很不容易!

0四、泛型通配符

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

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雖然支持泛型的向上轉型(使用 extends 通配符),但咱們卻沒法向其中添加任何東西——編譯器並不知道寵物小屋裏要住的是小貓仍是小狗,或者其餘寵物,所以乾脆什麼都不讓住。

看到這,你必定很是疑惑,既然PetHouse<? extends Pet>定義的寵物小屋什麼也不讓住,那爲何還要這樣定義呢?思考一下。

0五、讀者將軍的總結

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

上界不存的緣由:例如 List,編譯器只知道容器內是 Father 及其子類,具體是什麼類型並不知道,編譯器在看到 extends 後面的 Father 類,只是標上一個 CAP#1 做爲佔位符,不管往裏面插什麼,編譯器都不知道能不能和 CAP#1 匹配,因此就不容許插入。

extends的做用:能夠在初始化的時候存入一個值,而且能保證數據的穩定性,只能取不能存。讀取出來的數據能夠存在父類或者基類裏。

下界不取的緣由:下界限定了元素的最小粒度,其實是放鬆了容器元素的類型控制。例如 List, 元素是 Father 的基類,能夠存入 Father 及其子類。但編譯器並不知道哪一個是 Father 的超類,如 Human。讀取的時候,天然不知道是什麼類型,只能返回 Object,這樣元素信息就所有丟失了。

super的做用:用於參數類型限定。

PECS 原則:

1.頻繁往外讀取內容的,適合用extends
2.常常往裏插入的,適合用super


上一篇:Java 數組,看這篇就夠了

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

微信搜索「沉默王二」公衆號,關注後回覆「免費視頻」獲取 500G 高質量教學視頻(已分門別類)。

相關文章
相關標籤/搜索