本文源自參考《Think in Java》完沪,多篇博文以及閱讀源碼的總結(jié)
前言
Java中的泛型每各人都在使用饮怯,但是它底層的實現(xiàn)方法是什么呢勇蝙,為何要這樣實現(xiàn)延赌,這樣實現(xiàn)的優(yōu)缺點有哪些耽梅,怎么解決泛型帶來的問題薛窥。帶著好奇,我查閱資料進行了初步的學(xué)習(xí)褐墅,在此與諸位探討拆檬。
一 類型參數(shù)
學(xué)過JAVA的人都知道泛型洪己,明白大概怎么使用。在類上為:class 類名<T> {}竟贯,在方法上為:public <T> void 方法名 (T x){}答捕。泛型的實現(xiàn)使得類型變成了參數(shù)可以傳入,使得類功能多樣化屑那。
具體可分為5種情況:
- T是成員變量的類型
- T是泛型變量(無論成員變量還是局部變量)的類型參數(shù)拱镐,常見如
Class<T
>,List<T>
持际。 - T是方法拋出的Exception(要求
<T extends Exception>
) - T是方法的返回值
- T是方法的參數(shù)
1.1 泛型的實現(xiàn)
JAVA的泛型是基于編譯器實現(xiàn)的沃琅,使用了擦除的方法實現(xiàn),這是因為java1.5之后才出現(xiàn)了泛型蜘欲,為了保持向后兼容而做出的妥協(xié)益眉。
所謂擦除就是JAVA文件在編譯成字節(jié)碼時類型參數(shù)會被擦除掉,單獨記錄在其他地方姥份。并且用類型參數(shù)的父類代替原有的位置郭脂。
假設(shè)參數(shù)類型的占位符為T,擦除規(guī)則如下:
-
<T>
擦除后變?yōu)?code>Obecjt -
<? extends A>
擦除后變?yōu)?code>A -
<? super A>
擦除后變?yōu)?code>Object
這種規(guī)則叫做保留上界
編譯器擦除類型參數(shù)后澈歉,通過JAVA的強制轉(zhuǎn)換保證了類型參數(shù)在使用時的正確展鸡。如:在類型參數(shù)T中傳入了類A,那么編譯器會在所有類A將返回(拋出)類型參數(shù)T的代碼處加上(A)進行強轉(zhuǎn).
舉個栗子:
ArrayList<String> list = new ArrayList<String>();
list.add("123");
String b = list.get(0);
在編譯后會變成
ArrayList list = new ArrayList();//沒有參數(shù)即默認為Object
list.add("123");
String b = (String) list.get(0);
并且會在帶有類型參數(shù)類的子類中形成橋方法保證了多態(tài)性。
具體參考官方解釋如下
- Replace all type parameters in generic types with their bounds or Object if the type parameters are unbounded. The produced bytecode, therefore, contains only ordinary classes, interfaces, and methods.
- Insert type casts if necessary to preserve type safety.
- Generate bridge methods to preserve polymorphism in extended generic types.
二 通配符?
在帶有類型參數(shù)的類內(nèi)部埃难,代碼仍然按照參數(shù)類型擦除后的父類來處理莹弊。但是擦除存在一個問題,在這種機制下泛型是不變的涡尘,而沒有逆變和協(xié)變忍弛。
2.1 逆變與協(xié)變
協(xié)變和逆變網(wǎng)上有很多解釋,顯得模糊不清悟衩,我參考幾個編程語言的官方解釋后給出一個比較寬泛的定義剧罩。協(xié)變指能夠使用比原始聲明類型的派生程度更大(更具體的)的類型,逆變指能夠使用比原始聲明類型的派生程度更凶尽(不太具體的)的類型。
如:
Object obj = new String("123");
這就是協(xié)變幕与,將String這個更具體的(子類)類型賦給了原本較寬泛定義(父類)的類型Object挑势。
JAVA不允許將父類賦給子類,自然Java不支持逆變啦鸣。
網(wǎng)上很多博文說JAVA泛型也有逆變潮饱,我是不贊同的,那只是一種模擬的逆變诫给,即有部分逆變的特性而且看起來像逆變香拉,具體分析后文會給出
2.2 Java中的逆變與協(xié)變
在JAVA中啦扬,
List<Integer> b = new ArrayList<Integer>()
List<NumFber> a = b;
是無法通過編譯器檢查的。不允許這樣做有一個很充分的理由:這樣做將破壞要泛型的類型安全凫碌。如果能夠?qū)?code>List<Integer> 賦給List<Number>
扑毡。那么下面的代碼就允許將非Integer
的內(nèi)容放入 List<Integer>
:
List<Integer> b = new ArrayList<Integer>();
List<Number> a = b; // illegal
a.add(new Float(3.1415));
因為a
是List<Number>
,所以向其添加Float
似乎是完全可行的盛险。但是如果a
實際是List<Integer>
瞄摊,那么這就破壞了蘊含在b
中定義的類型聲明 —— 它是一個整數(shù)列表,這就是泛型類型不能協(xié)變的原因苦掘。但也因此使得泛型失去了多態(tài)的拓展性换帜。
2.3 通配符解決協(xié)變
Java官方通過加入了通配符?
來解決泛型協(xié)變的問題。這樣就能通過編譯了:
List<Integer> b = new ArrayList<Integer>();
List<? extends Number> a = b;
可以解讀為a
是一種帶有Number
的List
集合類鹤啡,在從a
中取出數(shù)據(jù)的時候統(tǒng)一當(dāng)做Number
處理就行了惯驼。同時這也是符合里氏替換原則的
但是編譯器會禁止你將將類Integer
放入a,即a.add(new Integer(1))//illegal
這也很合理,因為你聲明的a
本來就沒有限定a
包含的具體是哪個Number
子類递瑰,因此不準任何變量的添加保證了泛型的安全性祟牲。
解決往a
添加對象的方法也很簡單
List<Object> b = new ArrayList<Object>();
List<? super Number> a = b;
a
是某種Number
父類的List
集合類,將ArrayList<Object>
賦給a
也是合情合理的泣矛,Object
確實是Number
的父類疲眷。這也符合里氏替換原則的
(網(wǎng)上大部分博文說這就是逆變,但是仔細想想逆變的官方定義,在JAVA中可以理解為:類T
是類S
的子類您朽,而類A<T>
是類A<S>
的父類狂丝。仔細看看List<? super Number>
和List<Object>
的關(guān)系,在這里T
是Number
,而S
是Object
哗总,但是List<? super Number>
從邏輯上來看真的是List<Object>
的子類嗎几颜,如果單純從字面上來看List<? super Number>
是帶有Number
父類的集合類,根據(jù)保留上界的擦除方法,應(yīng)該擦除為List<Object>
,將一個List<Object>
賦給另一個List<Object>
是不存在任何逆變的讯屈。我在疑惑之下進行了科學(xué)上網(wǎng)查閱資料蛋哭,也沒有英文資料說明JAVA泛型里這屬于逆變)