參考地址:
《Java 泛型镜硕,你了解類型擦除嗎闻妓?》
《Java中的逆變與協(xié)變》
《java 泛型中 T、E ... 和 問號(通配符)的區(qū)別》
注:本文是在 frank909 的文章 《Java 泛型渗常,你了解類型擦除嗎壮不?》 的基礎(chǔ)上,參考其他文章并重新總結(jié)排版而來的皱碘。
一. 泛型
泛型的英文是 generics询一,generic 的意思是通用,而翻譯成中文癌椿,“泛”應(yīng)該意為廣泛健蕊,型是類型。所以泛型就是能廣泛適用的類型如失。
1.1 泛型的定義
泛型還有一種較為準(zhǔn)確的說法就是為了參數(shù)化類型绊诲,或者說可以將類型當(dāng)作參數(shù)傳遞給一個類或者是方法。
那么褪贵,如何解釋類型參數(shù)化呢?
public class Cache {
Object value;
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}
假設(shè) Cache 能夠存取任何類型的值抗俄,于是脆丁,我們可以這樣使用它。
Cache cache = new Cache();
cache.setValue(134);
int value = (int) cache.getValue();
cache.setValue("hello");
String value1 = (String) cache.getValue();
使用的方法也很簡單动雹,只要我們做正確的強(qiáng)制轉(zhuǎn)換就好了槽卫。
但是,泛型卻給我們帶來了不一樣的編程體驗(yàn)胰蝠。
public class Cache<T> {
T value;
public Object getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
這就是泛型歼培,它將 value 這個屬性的類型也參數(shù)化了,這就是所謂的參數(shù)化類型茸塞。再看它的使用方法躲庄。
Cache<String> cache1 = new Cache<String>();
cache1.setValue("123");
String value2 = cache1.getValue();
Cache<Integer> cache2 = new Cache<Integer>();
cache2.setValue(456);
int value3 = cache2.getValue();
最顯而易見的好處就是它不再需要對取出來的結(jié)果進(jìn)行強(qiáng)制轉(zhuǎn)換了。但钾虐,還有另外一點(diǎn)不同噪窘。
泛型除了可以將類型參數(shù)化外,而參數(shù)一旦確定好效扫,如果類似不匹配倔监,編譯器就不通過直砂。
上面代碼顯示,無法將一個 String 對象設(shè)置到 cache2 中浩习,因?yàn)榉盒妥屗唤邮?Integer 的類型静暂。
所以,綜合上面信息谱秽,我們可以得到下面的結(jié)論籍嘹。
- 與普通的 Object 代替一切類型這樣簡單粗暴而言,泛型使得數(shù)據(jù)的類別可以像參數(shù)一樣由外部傳遞進(jìn)來弯院。它提供了一種擴(kuò)展能力辱士。它更符合面向抽象開發(fā)的軟件編程宗旨。
- 當(dāng)具體的類型確定后听绳,泛型又提供了一種類型檢測的機(jī)制颂碘,只有相匹配的數(shù)據(jù)才能正常的賦值,否則編譯器就不通過椅挣。所以說头岔,它是一種類型安全檢測機(jī)制,一定程度上提高了軟件的安全性防止出現(xiàn)低級的失誤鼠证。
- 泛型提高了程序代碼的可讀性峡竣,不必要等到運(yùn)行的時候才去強(qiáng)制轉(zhuǎn)換,在定義或者實(shí)例化階段量九,因?yàn)?Cache< String > 這個類型顯化的效果适掰,程序員能夠一目了然猜測出代碼要操作的數(shù)據(jù)類型。
下面的文章荠列,我們正常介紹泛型的相關(guān)知識类浪。
1.2 泛型的使用
泛型按照使用情況可以分為 3 種:
- 泛型類。
- 泛型方法肌似。
- 泛型接口费就。
1.2.1 泛型類
我們可以這樣定義一個泛型類。
public class Test<T> {
T field1;
}
尖括號 < > 中的 T 被稱作是類型參數(shù)川队,用于指代任何類型力细。事實(shí)上,T 只是一種習(xí)慣性寫法固额,如果你愿意眠蚂。你可以這樣寫。
public class Test<Hello> {
Hello field1;
}
但出于規(guī)范的目的对雪,Java 還是建議我們用單個大寫字母來代表類型參數(shù)河狐。常見的如:
- T 代表一般的任何類。
- E 代表 Element 的意思,或者 Exception 異常的意思馋艺。
- K 代表 Key 的意思栅干。
- V 代表 Value 的意思,通常與 K 一起配合使用捐祠。
- S 代表 Subtype 的意思碱鳞,文章后面部分會講解示意。
如果一個類被 <T> 的形式定義踱蛀,那么它就被稱為是泛型類窿给。
那么對于泛型類怎么樣使用呢?
Test<String> test1 = new Test<>();
Test<Integer> test2 = new Test<>();
只要在對泛型類創(chuàng)建實(shí)例的時候率拒,在尖括號中賦值相應(yīng)的類型便是崩泡。T 就會被替換成對應(yīng)的類型,如 String 或者是 Integer猬膨。你可以相像一下角撞,當(dāng)一個泛型類被創(chuàng)建時,內(nèi)部自動擴(kuò)展成下面的代碼勃痴。
public class Test<String> {
String field1;
}
當(dāng)然谒所,泛型類不至接受一個類型參數(shù),它還可以這樣接受多個類型參數(shù)沛申。
public class MultiType <E,T>{
E value1;
T value2;
public E getValue1(){
return value1;
}
public T getValue2(){
return value2;
}
}
1.2.2 泛型方法
public class Test1 {
public <T> void testMethod(T t){
}
}
泛型方法與泛型類稍有不同的地方是劣领,類型參數(shù)也就是尖括號那一部分是寫在返回值前面的。<T> 中的 T 被稱為類型參數(shù)铁材,而方法中的 T 被稱為參數(shù)化類型哪审,它不是運(yùn)行時真正的參數(shù)掘剪。
當(dāng)然馒稍,聲明的類型參數(shù)蹈矮,其實(shí)也是可以當(dāng)作返回值的類型的洒宝。
public <T> T testMethod1(T t){
return null;
}
1.2.3 泛型類與泛型方法共存
public class Test1<T>{
public void testMethod(T t){
System.out.println(t.getClass().getName());
}
public <T> T testMethod1(T t){
return t;
}
}
上面代碼中厨疙,Test1< T > 是泛型類薇正,testMethod 是泛型類中的普通方法洛心,而 testMethod1 是一個泛型方法缴守。而泛型類中的類型參數(shù)與泛型方法中的類型參數(shù)是沒有相應(yīng)的聯(lián)系的葬毫,泛型方法始終以自己定義的類型參數(shù)為準(zhǔn)。
所以屡穗,針對上面的代碼贴捡,我們可以這樣編寫測試代碼。
Test1<String> t = new Test1();
t.testMethod("generic");
Integer i = t.testMethod1(new Integer(1));
泛型類的實(shí)際類型參數(shù)是 String村砂,而傳遞給泛型方法的類型參數(shù)是 Integer烂斋,兩者不想干。
但是,為了避免混淆汛骂,如果在一個泛型類中存在泛型方法罕模,那么兩者的類型參數(shù)最好不要同名。比如帘瞭,Test1< T > 代碼可以更改為這樣:
public class Test1<T>{
public void testMethod(T t){
System.out.println(t.getClass().getName());
}
public <E> E testMethod1(E e){
return e;
}
}
1.2.4 泛型接口
泛型接口和泛型類差不多淑掌,所以一筆帶過。
public interface Iterable<T> {
}
1.3 泛型的類型擦除
泛型是 Java 1.5 版本才引進(jìn)的概念蝶念,在這之前是沒有泛型的概念的抛腕,但顯然,泛型代碼能夠很好地和之前版本的代碼很好地兼容媒殉。
這是因?yàn)椋?strong>泛型信息只存在于代碼編譯階段担敌,在進(jìn)入 JVM 之前,與泛型相關(guān)的信息會被擦除掉廷蓉,專業(yè)術(shù)語叫做類型擦除全封。
通俗地講,泛型類和普通類在 java 虛擬機(jī)內(nèi)是沒有什么特別的地方苦酱∈勖玻回顧文章開始時的那段代碼
List<String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();
System.out.println(l1.getClass() == l2.getClass());
打印的結(jié)果為 true 是因?yàn)?List< String > 和 List< Integer > 在 jvm 中的 Class 都是 List.class。
泛型信息被擦除了疫萤。
可能同學(xué)會問颂跨,那么類型 String 和 Integer 怎么辦?
答案是泛型轉(zhuǎn)譯扯饶。
public class Erasure <T>{
T object;
public Erasure(T object) {
this.object = object;
}
}
Erasure 是一個泛型類恒削,我們查看它在運(yùn)行時的狀態(tài)信息可以通過反射。
Erasure<String> erasure = new Erasure<String>("hello");
Class eclz = erasure.getClass();
System.out.println("erasure class is:"+eclz.getName());
打印的結(jié)果是
erasure class is:com.frank.test.Erasure
Class 的類型仍然是 Erasure 并不是 Erasure<T> 這種形式尾序,那我們再看看泛型類中 T 的類型在 jvm 中是什么具體類型钓丰。
Field[] fs = eclz.getDeclaredFields();
for ( Field f:fs) {
System.out.println("Field name "+f.getName()+" type:"+f.getType().getName());
}
打印結(jié)果是
Field name object type:java.lang.Object
那我們可不可以說,泛型類被類型擦除后每币,相應(yīng)的類型就被替換成 Object 類型呢携丁?
這種說法,不完全正確兰怠。
我們更改一下代碼梦鉴。
public class Erasure <T extends String>{
// public class Erasure <T>{
T object;
public Erasure(T object) {
this.object = object;
}
}
現(xiàn)在再看測試結(jié)果:
Field name object type:java.lang.String
我們現(xiàn)在可以下結(jié)論了,在泛型類被類型擦除的時候揭保,之前泛型類中的類型參數(shù)部分如果沒有指定上限肥橙,如 < T > 則會被轉(zhuǎn)譯成普通的 Object 類型,如果指定了上限如 < T extends String> 則類型參數(shù)就被替換成類型上限秸侣。
所以存筏,在反射中宠互。
public class Erasure <T>{
T object;
public Erasure(T object) {
this.object = object;
}
public void add(T object){
}
}
add() 這個方法對應(yīng)的 Method 的簽名應(yīng)該是 Object.class。
Erasure<String> erasure = new Erasure<String>("hello");
Class eclz = erasure.getClass();
System.out.println("erasure class is:"+eclz.getName());
Method[] methods = eclz.getDeclaredMethods();
for ( Method m:methods ){
System.out.println(" method:"+m.toString());
}
打印結(jié)果是
method:public void com.frank.test.Erasure.add(java.lang.Object)
也就是說椭坚,如果你要在反射中找到 add 對應(yīng)的 Method予跌,你應(yīng)該調(diào)用 getDeclaredMethod("add",Object.class) 否則程序會報錯,提示沒有這么一個方法藕溅,原因就是類型擦除的時候匕得,T 被替換成 Object 類型了。
1.4 泛型類型擦除的局限性
類型擦除巾表,是泛型能夠與之前的 java 版本代碼兼容共存的原因汁掠。但也因?yàn)轭愋筒脸鼤ǖ艉芏嗬^承相關(guān)的特性集币,這是它帶來的局限性考阱。
理解類型擦除有利于我們繞過開發(fā)當(dāng)中可能遇到的雷區(qū),同樣理解類型擦除也能讓我們繞過泛型本身的一些限制鞠苟。比如:
正常情況下乞榨,因?yàn)榉盒偷南拗疲幾g器不讓最后一行代碼編譯通過当娱,因?yàn)轭愃撇黄ヅ涑约龋牵趯︻愋筒脸牧私饪缦福梅瓷漯幸校覀兛梢岳@過這個限制。
public interface List<E> extends Collection<E>{
boolean add(E e);
}
上面是 List 和其中的 add() 方法的源碼定義冀惭。
因?yàn)?E 代表任意的類型震叙,所以類型擦除時,add 方法其實(shí)等同于
boolean add(Object obj);
那么散休,利用反射媒楼,我們繞過編譯器去調(diào)用 add 方法。
public class ToolTest {
public static void main(String[] args) {
List<Integer> ls = new ArrayList<>();
ls.add(23);
// ls.add("text");
try {
Method method = ls.getClass().getDeclaredMethod("add",Object.class);
method.invoke(ls,"test");
method.invoke(ls,42.9f);
} catch (NoSuchMethodException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (SecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalAccessException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalArgumentException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InvocationTargetException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
for ( Object o: ls){
System.out.println(o);
}
}
}
打印結(jié)果是:
23
test
42.9
可以看到戚丸,利用類型擦除的原理划址,用反射的手段就繞過了正常開發(fā)中編譯器不允許的操作限制。
1.5 泛型需注意問題
1.5.1 泛型不接受 8 種基本數(shù)據(jù)類型
泛型類或者泛型方法中限府,不接受 8 種基本數(shù)據(jù)類型猴鲫。所以,你沒有辦法進(jìn)行這樣的編碼:
List<int> li = new ArrayList<>();
List<boolean> li = new ArrayList<>();
需要使用它們對應(yīng)的包裝類谣殊。
List<Integer> li = new ArrayList<>();
List<Boolean> li1 = new ArrayList<>();
對泛型方法的困惑
public <T> T test(T t){
return null;
}
有的同學(xué)可能對于連續(xù)的兩個 T 感到困惑,其實(shí) < T > 是為了說明類型參數(shù)牺弄,是聲明,而后面的不帶尖括號的 T 是方法的返回值類型姻几。
你可以相像一下,如果 test() 這樣被調(diào)用
test("123");
那么實(shí)際上相當(dāng)于
public String test(String t);
1.5.2 Java 不能創(chuàng)建具體類型的泛型數(shù)組
這句話可能難以理解,代碼說明蛇捌。
List<Integer>[] li2 = new ArrayList<Integer>[];
List<Boolean> li3 = new ArrayList<Boolean>[];
這兩行代碼是無法在編譯器中編譯通過的抚恒。原因還是類型擦除帶來的影響。
List< Integer> 和 List< Boolean> 在 jvm 中等同于List< Object> 络拌,所有的類型信息都被擦除俭驮,程序也無法分辨一個數(shù)組中的元素類型具體是 List< Integer>類型還是 List< Boolean> 類型。
但是春贸,
List<?>[] li3 = new ArrayList<?>[10];
li3[1] = new ArrayList<String>();
List<?> v = li3[1];
借助于無限定通配符卻可以混萝,前面講過 ? 代表未知類型萍恕,所以它涉及的操作都基本上與類型無關(guān)逸嘀,因此 jvm 不需要針對它對類型作判斷,因此它能編譯通過允粤,但是崭倘,只提供了數(shù)組中的元素因?yàn)橥ㄅ浞颍荒茏x类垫,不能寫司光。比如,上面的 v 這個局部變量悉患,它只能進(jìn)行 get() 操作残家,不能進(jìn)行 add() 操作,這個在前面通配符的內(nèi)容小節(jié)中已經(jīng)講過购撼。
二. 通配符 "?"
除了用 < T > 表示泛型外跪削,還有 < ? > 這種形式。迂求? 被稱為通配符碾盐。
2.1 通配符 "?" 簡介
可能有同學(xué)會想,已經(jīng)有了 < T > 的形式了揩局,為什么還要引進(jìn) < ? > 這樣的概念呢毫玖?
class Base{}
class Sub extends Base{}
Sub sub = new Sub();
Base base = sub;
上面代碼顯示,Base 是 Sub 的父類凌盯,它們之間是繼承關(guān)系付枫,所以 Sub 的實(shí)例可以給一個 Base 引用賦值,那么
List<Sub> lsub = new ArrayList<>();
List<Base> lbase = lsub;
最后一行代碼成立嗎驰怎?編譯會通過嗎阐滩?
答案是否定的。
編譯器不會讓它通過的县忌。Sub 是 Base 的子類掂榔,不代表 List<Sub> 和 List<Base> 有繼承關(guān)系继效。
但是,在現(xiàn)實(shí)編碼中装获,確實(shí)有這樣的需求瑞信,希望泛型能夠處理某一范圍內(nèi)的數(shù)據(jù)類型,比如某個類和它的子類穴豫,對此 Java 引入了通配符這個概念凡简。
所以,通配符的出現(xiàn)是為了指定泛型中的類型范圍精肃。
2.2 通配符的協(xié)變與逆變
通配符有 3 種形式秤涩。
- < ?> 被稱作無限定的通配符。
- < ? extends T> 被稱作有上限的通配符肋杖。
- < ? super T> 被稱作有下限的通配符溉仑。
2.2.1 無限定通配符
public void testWildCards(Collection<?> collection){
}
上面的代碼中,方法內(nèi)的參數(shù)是被無限定通配符修飾的 Collection 對象状植,它隱略地表達(dá)了一個意圖或者可以說是限定浊竟,那就是 testWidlCards() 這個方法內(nèi)部無需關(guān)注 Collection 中的真實(shí)類型,因?yàn)樗俏粗慕蚧K哉穸ǎ阒荒苷{(diào)用 Collection 中與類型無關(guān)的方法。
我們可以看到肉拓,當(dāng) < ? > 存在時后频,Collection 對象喪失了 add() 方法的功能,編譯器不通過暖途。
我們再看代碼卑惜。
List<?> wildlist = new ArrayList<String>();
wildlist.add(123);// 編譯不通過
有人說,< ? > 提供了只讀的功能驻售,也就是它刪減了增加具體類型元素的能力露久,只保留與具體類型無關(guān)的功能。它不管裝載在這個容器內(nèi)的元素是什么類型欺栗,它只關(guān)心元素的數(shù)量毫痕、容器是否為空?我想這種需求還是很常見的吧迟几。
有同學(xué)可能會想消请,< ? > 既然作用這么渺小,那么為什么還要引用它呢类腮? ?
個人認(rèn)為臊泰,提高了代碼的可讀性,程序員看到這段代碼時蚜枢,就能夠迅速對此建立極簡潔的印象因宇,能夠快速推斷源碼作者的意圖七婴。
2.2.2 協(xié)變 < ? extends T>
Java 中 String 類型是繼承自 Object 的,姑且記做 String ≦ Object察滑,表示 String 是 Object 的子類型,String 的對象可以賦給 Object 的對象修肠。而 Object 的數(shù)組類型 Object[]贺辰,理解成是由 Object 構(gòu)造出來的一種新的類型,可以認(rèn)為是一種構(gòu)造類型嵌施,記f(Object)饲化,那么可以這么來描述協(xié)變和逆變:
- 當(dāng) A ≦ B 時,如果有 f(A) ≦ f(B)吗伤,那么 f 叫做協(xié)變吃靠;
- 當(dāng) A ≦ B 時,如果有 f(B) ≦ f(A)足淆,那么 f 叫做逆變巢块;
- 如果上面兩種關(guān)系都不成立,則叫做不可變巧号。
< ? extends> 實(shí)現(xiàn)了泛型的協(xié)變族奢,比如:
List<? extends Number> list = new ArrayList<>();
“? extends Number” 則表示通配符 "?" 的上界為 Number,換句話說丹鸿,就是“? extends Number”可以代表 Number 或其子類越走,但代表不了 Number 的父類(如Object),因?yàn)橥ㄅ浞纳辖缡?Number靠欢。
于是有 "? extends Number" ≦ Number廊敌,則 List< ? extends Number> ≦ List< Number >。那么就有:
List<? extends Number> list001 = new ArrayList<Integer>();
List<? extends Number> list002 = new ArrayList<Float>();
但是這里不能向 list001, list002 添加除 null 以外的任意對象门怪÷獬海可以這樣理解一下,List< Integer > 可以添加 Interger 及其子類薪缆,List< Float >可以添加 Float 及其子類秧廉,List< Integer >, List< Float > 都是 List< ? extends Number >的子類型,如果能將 Float 的子類添加到 List< ? extends Number >中拣帽,就說明 Float 的子類也是可以添加到 List< Integer >中的疼电,顯然是不可行。故 Java 為了保護(hù)其類型一致减拭,禁止向 List< ? extends Number >添加任意對象蔽豺,不過卻可以添加 null。
2.2.3 逆變 < ? super >
< ? super > 實(shí)現(xiàn)了泛型的逆變拧粪,比如:
List<? super Number> list = new ArrayList<>();
"? super Number" 則表示通配符 "?" 的下界為 Number修陡。為了保護(hù)類型的一致性沧侥,因?yàn)?"? super Number" 可以是 Object 或其他 Number 的父類,因無法確定其類型魄鸦,也就不能往List< ? super Number >添加 Number 的任意父類對象宴杀。但是可以向 List< ? super Number >添加 Number 及其子類。
List<? super Number> list001 = new ArrayList<Number>();
List<? super Number> list002 = new ArrayList<Object>();
list001.add(new Integer(3));
list002.add(new Integer(3));
三. 通配符與泛型的區(qū)別
筆者參考了 《Java 泛型拾因,你了解類型擦除嗎旺罢?》 與 《java 泛型中 T、E ... 和 問號(通配符)的區(qū)別》 兩篇文章中對于通配符與泛型的區(qū)別绢记,兩篇文章中筆者認(rèn)為后者的描述比較淺顯易懂扁达,前者的行文并沒有怎么理解。所以筆者比較推薦后者敘述的通配符與泛型的區(qū)別蠢熄。
首先跪解,泛型中 T, E 等符號是泛型類、泛型方法定義時候用的签孔。泛型類的定義緊跟在類名后面叉讥;泛型方法的定義緊跟修飾符后面(public)。
- 泛型類:public class TestClassDefine<T>{}
- 泛型方法:public <T> T testGenericMethodDefine(T t){}
通配符 "?"骏啰,通常是在變量賦值或變量聲明時用的节吮。
List<?> unknownList;
List<? extends Number> unknownNumberList;
List<? super Integer> unknownBaseLineIntgerList;