《Java編程思想》第四版足足用了75頁來說泛型——厚厚的一沓內容,很容易讓人頭大——但其實根本不用這麼多,只須要一句話:我是一個泛型隊列,狗能夠站進來,貓也能夠站進來,但最好不要既站貓,又站狗!html
泛型,有人拆解這個詞爲「參數化類型」。這種拆解其實也很差理解,仍是按照沉默王二的意思來理解一下吧。java
如今有一隻玻璃杯,你可讓它盛一杯白開水,也能夠盛一杯二鍋頭——泛型的概念就在於此,製造這隻杯子的時候不必在說明書上定義死,指明它只能盛白開水而不能盛二鍋頭!程序員
能夠在說明書上指明它用來盛裝液體,但最好也不要這樣,弄很差用戶想用它來盛幾塊冰糖呢!編程
這麼一說,你是否是感受不那麼抽象了?泛型其實就是在定義類、接口、方法的時候不侷限地指定某一種特定類型,而讓類、接口、方法的調用者來決定具體使用哪種類型的參數。數組
就比如,玻璃杯的製造者說,我不知道使用者用這隻玻璃杯來幹嗎,因此我只負責造這麼一隻杯子;玻璃杯的使用者說,這就對了,我來決定這隻玻璃杯是盛白開水仍是二鍋頭,或者冰糖。微信
咱們來看一段簡短的代碼: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中取出貓的時候,也再也不須要強制轉型了。
有人說,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時才引入了泛型,但爲了照顧之前設計上的缺陷,同時兼容非泛型的代碼,不得不作出了一個折中的策略:編譯時對泛型要求嚴格,運行時卻把泛型擦除了——要兼容之前的版本,還要升級擴展新的功能,真的很不容易!
有些時候,你會見到這樣一些代碼:
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>
定義的寵物小屋什麼也不讓住,那爲何還要這樣定義呢?思考一下。
泛型限定符有一描述:上界不存下界不取。
上界不存的緣由:例如 List,編譯器只知道容器內是 Father 及其子類,具體是什麼類型並不知道,編譯器在看到 extends 後面的 Father 類,只是標上一個 CAP#1
做爲佔位符,不管往裏面插什麼,編譯器都不知道能不能和 CAP#1
匹配,因此就不容許插入。
extends的做用:能夠在初始化的時候存入一個值,而且能保證數據的穩定性,只能取不能存。讀取出來的數據能夠存在父類或者基類裏。
下界不取的緣由:下界限定了元素的最小粒度,其實是放鬆了容器元素的類型控制。例如 List, 元素是 Father 的基類,能夠存入 Father 及其子類。但編譯器並不知道哪一個是 Father 的超類,如 Human。讀取的時候,天然不知道是什麼類型,只能返回 Object,這樣元素信息就所有丟失了。
super的做用:用於參數類型限定。
PECS 原則:
1.頻繁往外讀取內容的,適合用extends
2.常常往裏插入的,適合用super
上一篇:Java 數組,看這篇就夠了
微信搜索「沉默王二」公衆號,關注後回覆「免費視頻」獲取 500G 高質量教學視頻(已分門別類)。