溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

Java泛型的重要目的:別讓貓別站在狗隊(duì)里

發(fā)布時(shí)間:2020-07-20 06:22:18 來(lái)源:網(wǎng)絡(luò) 閱讀:98 作者:沉默王二 欄目:編程語(yǔ)言

《Java編程思想》第四版足足用了75頁(yè)來(lái)講泛型——厚厚的一沓內(nèi)容,很容易讓人頭大——但其實(shí)根本不用這么多,只需要一句話:我是一個(gè)泛型隊(duì)列,狗可以站進(jìn)來(lái),貓也可以站進(jìn)來(lái),但最好不要既站貓,又站狗!

01、泛型是什么

泛型,有人拆解這個(gè)詞為“參數(shù)化類(lèi)型”。這種拆解其實(shí)也不好理解,還是按照沉默王二的意思來(lái)理解一下吧。

現(xiàn)在有一只玻璃杯,你可以讓它盛一杯白開(kāi)水,也可以盛一杯二鍋頭——泛型的概念就在于此,制造這只杯子的時(shí)候沒(méi)必要在說(shuō)明書(shū)上定義死,指明它只能盛白開(kāi)水而不能盛二鍋頭!

可以在說(shuō)明書(shū)上指明它用來(lái)盛裝液體,但最好也不要這樣,弄不好用戶想用它來(lái)盛幾塊冰糖呢!

這么一說(shuō),你是不是感覺(jué)不那么抽象了?泛型其實(shí)就是在定義類(lèi)、接口、方法的時(shí)候不局限地指定某一種特定類(lèi)型,而讓類(lèi)、接口、方法的調(diào)用者來(lái)決定具體使用哪一種類(lèi)型的參數(shù)。

就好比,玻璃杯的制造者說(shuō),我不知道使用者用這只玻璃杯來(lái)干嘛,所以我只負(fù)責(zé)造這么一只杯子;玻璃杯的使用者說(shuō),這就對(duì)了,我來(lái)決定這只玻璃杯是盛白開(kāi)水還是二鍋頭,或者冰糖。

02、什么時(shí)候用泛型

我們來(lái)看一段簡(jiǎn)短的代碼:

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);
    }

}

這段代碼的意思是:我們?cè)趍ap中放了一只狗(Dog),又放了一只貓(Cat),當(dāng)我們想從map中取出貓的時(shí)候,卻一不留神把狗取了出來(lái)。

這段代碼編譯是沒(méi)有問(wèn)題的,但運(yùn)行的時(shí)候就會(huì)報(bào)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)

為什么會(huì)這樣呢?

1)寫(xiě)代碼的程序員粗心大意。要從map中把貓取出來(lái),你不能取狗啊!

2)創(chuàng)建map的時(shí)候,沒(méi)有明確指定map中要放的類(lèi)型。如果指定是要放貓,那肯定取的時(shí)候就是貓,不會(huì)取出來(lái)狗;如果指定是要放狗,也一個(gè)道理。

第一種情況不太好解決,總不能把程序員打一頓(我可不想做一個(gè)天天背鍋的程序員,很重的好不好);第二種情況就比較容易解決,因?yàn)镸ap支持泛型(泛型接口)。

public interface Map<K,V> {
}

注:在Java中,經(jīng)常用T、E、K、V等形式的參數(shù)來(lái)表示泛型參數(shù)。

T:代表一般的任何類(lèi)。
E:代表 Element 的意思,或者 Exception 異常的意思。
K:代表 Key 的意思。
V:代表 Value 的意思,通常與 K 一起配合使用。

既然Map支持泛型,那作為Map的實(shí)現(xiàn)者HashMap(泛型類(lèi))也支持泛型了。

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);
}

好了,現(xiàn)在使用泛型的形式來(lái)定義一個(gè)只能放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);
    }
}

當(dāng)使用泛型定義map(鍵為String類(lèi)型,值為Cat類(lèi)型)后:

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

2)當(dāng)你從map中取出貓的時(shí)候,也不再需要強(qiáng)制轉(zhuǎn)型了。

03、類(lèi)型擦除

有人說(shuō),Java的泛型做的只是表面功夫——泛型信息存在于編譯階段(狗隊(duì)在編譯時(shí)不允許站貓),運(yùn)行階段就消失了(運(yùn)行時(shí)的隊(duì)列里沒(méi)有貓的信息,連狗的信息也沒(méi)有)——這種現(xiàn)象被稱(chēng)為“類(lèi)型擦除”。

來(lái),看代碼解釋一下:

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;否則編譯器會(huì)提醒The method put(String, Cmower.Cat) in the type Map&lt;String,Cmower.Cat&gt; is not applicable for the arguments (String, Cmower.Dog)。編譯器做得不錯(cuò),值得點(diǎn)贊。

但是問(wèn)題就來(lái)了,map的Class類(lèi)型為HashMap,map1的Class類(lèi)型也為HashMap——也就是說(shuō),Java代碼在運(yùn)行的時(shí)候并不知道m(xù)ap的鍵位上放的是Cat,map1的鍵位上放的是Dog。

那么,試著想一些可怕的事情:既然運(yùn)行時(shí)泛型的信息被擦除了,而反射機(jī)制是在運(yùn)行時(shí)確定類(lèi)型信息的,那么利用反射機(jī)制,是不是就能夠在鍵位為Cat的Map上放一只Dog呢?

我們不妨來(lái)試一試:

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();
        }
    }

}

看到?jīng)]?我們竟然在鍵位為Cat的Map上放了一只Dog!

注:Java的設(shè)計(jì)者在JDK 1.5時(shí)才引入了泛型,但為了照顧以前設(shè)計(jì)上的缺陷,同時(shí)兼容非泛型的代碼,不得不做出了一個(gè)折中的策略:編譯時(shí)對(duì)泛型要求嚴(yán)格,運(yùn)行時(shí)卻把泛型擦除了——要兼容以前的版本,還要升級(jí)擴(kuò)展新的功能,真的很不容易!

04、泛型通配符

有些時(shí)候,你會(huì)見(jiàn)到這樣一些代碼:

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

?和關(guān)鍵字extends或者super在一起其實(shí)就是泛型的高級(jí)應(yīng)用:通配符。

我們來(lái)自定義一個(gè)泛型類(lèi)——PetHouse(寵物小屋),它有一些基本的動(dòng)作(可以住進(jìn)來(lái)一只寵物,也可以放出去):

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);
    }
}

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

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

然后,我們讓小貓和小狗住進(jìn)去:

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

如果我們只想要住進(jìn)去一只小貓,打算這樣定義小屋:

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

但事實(shí)上,編譯器不允許我們這樣定義:因?yàn)榉盒筒恢苯又С窒蛏限D(zhuǎn)型。該怎么辦呢?

可以這樣定義小屋:

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

也就是說(shuō),寵物小屋可以住進(jìn)去小貓,但它必須是寵物(Pet或者Pet的子類(lèi))而不是一只野貓。

但很遺憾,這個(gè)寵物小屋實(shí)際上住不了小貓,看下圖。

Java泛型的重要目的:別讓貓別站在狗隊(duì)里

這是因?yàn)镴ava雖然支持泛型的向上轉(zhuǎn)型(使用 extends 通配符),但我們卻無(wú)法向其中添加任何東西——編譯器并不知道寵物小屋里要住的是小貓還是小狗,或者其他寵物,因此干脆什么都不讓住。

看到這,你一定非常疑惑,既然PetHouse&lt;? extends Pet&gt;定義的寵物小屋什么也不讓住,那為什么還要這樣定義呢?思考一下。

05、讀者將軍的總結(jié)

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

上界不存的原因:例如 List,編譯器只知道容器內(nèi)是 Father 及其子類(lèi),具體是什么類(lèi)型并不知道,編譯器在看到 extends 后面的 Father 類(lèi),只是標(biāo)上一個(gè) CAP#1 作為占位符,無(wú)論往里面插什么,編譯器都不知道能不能和 CAP#1 匹配,所以就不允許插入。

extends的作用:可以在初始化的時(shí)候存入一個(gè)值,并且能保證數(shù)據(jù)的穩(wěn)定性,只能取不能存。讀取出來(lái)的數(shù)據(jù)可以存在父類(lèi)或者基類(lèi)里。

下界不取的原因:下界限定了元素的最小粒度,實(shí)際上是放松了容器元素的類(lèi)型控制。例如 List, 元素是 Father 的基類(lèi),可以存入 Father 及其子類(lèi)。但編譯器并不知道哪個(gè)是 Father 的超類(lèi),如 Human。讀取的時(shí)候,自然不知道是什么類(lèi)型,只能返回 Object,這樣元素信息就全部丟失了。

super的作用:用于參數(shù)類(lèi)型限定。

PECS 原則:

1.頻繁往外讀取內(nèi)容的,適合用extends
2.經(jīng)常往里插入的,適合用super


上一篇:Java 數(shù)組,看這篇就夠了

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

微信搜索「沉默王二」公眾號(hào),關(guān)注后回復(fù)「免費(fèi)視頻」獲取 500G 高質(zhì)量教學(xué)視頻(已分門(mén)別類(lèi))。

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI