泛型的類型安全性
有許多原因促成了泛型的出現(xiàn),而最引人注意的一個原因,就是為了創(chuàng)建容器類。
如果沒有泛型,如果我們需要實(shí)現(xiàn)一個通用的隊(duì)列,那么只能使用Obejct數(shù)組去實(shí)現(xiàn),并且add方法的參數(shù)和get方法的返回值都為Object:
public class MyList {
private Object[] mData;
public void add(Object obj) {
...
}
public Object get(int index) {
...
}
...
}
但是這樣的話其實(shí)是很不安全的,類型安全需要靠用戶去自己維護(hù)。但用戶往往都是愚蠢的:
MyList myList = new MyList();
myList.add("1");
myList.add("2");
myList.add(3);
String val1 = (String) myList.get(0);
String val2 = (String) myList.get(1);
String val3 = (String) myList.get(2);
上面的代碼在編譯的時(shí)候沒有問題,但是真正運(yùn)行的時(shí)候程序跑著跑著就掛了,這就叫做類型不安全的設(shè)計(jì)。
使用泛型的意義在于它是類型安全的,如果使用泛型規(guī)定了參數(shù)和返回值的類型的話,上面的代碼在編譯的時(shí)候就會失敗:
public class MyList<E> {
private Object[] mData;
...
public void add(E obj) {
...
}
public E get(int index) {
...
return (E) mData[index];
}
}
MyList<String> myList = new MyList<>();
myList.add("1");
myList.add("2");
myList.add(3); //這里會編譯失敗
String val1 = myList.get(0);
String val2 = myList.get(1);
String val3 = myList.get(0);
類型標(biāo)識符
在MyList<E>聲明尖括號里面的就是類型標(biāo)識符,它其實(shí)是一個占位符,代表了某個類型,我們在類里面就能用這個占位符代表某種類型。例如add方法的參數(shù)或者get的返回值,當(dāng)然也能用來聲明一個成員變量。
可能有人會說經(jīng)??吹蕉际怯肨泛型作為泛型標(biāo)識符,為什么這里我們用E呢?
其實(shí)用什么字母做標(biāo)識符在java里面并沒有硬性規(guī)定,甚至你也可以不用僅一個字符,用一個單詞也是可以的。
不過我們通常會按照習(xí)慣在不同場景下用不同的字母標(biāo)識符:
- E - Element (在集合中使用)
- T - Type(Java 類)
- K - Key(鍵)
- V - Value(值)
泛型通配符
在泛型中有個很重要的知識點(diǎn)就是泛型類型之間是不具有繼承關(guān)系的,也就是說List<Object>并不是List<String>的父類:
public void printList(List<Object> list) {
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}
List<String> strList = Arrays.asList("a", "b", "c", "d", "e");
printList(strList); //錯誤,List<Object>不是List<String>的父類
為了實(shí)現(xiàn)上面的printList方法,類型通配符就出現(xiàn)了:
public void printList(List<?> list) {
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}
List<String> strList = Arrays.asList("a", "b", "c", "d", "e");
printList(strList);
List<?>可以匹配List<String>、List<Integer>等等的各種類型。
大家有可能會聽過類型通配符上限和下限,這兩個東西是怎樣的概念呢?有時(shí)候我們會需要限定只能傳入某些型的子類或者父類的容器:
上限:<? extends T> 只能匹配T和T的子類
下限:<? super T> 只能匹配T和T的父類
//只能傳入ClassA的子類的容器
public void printList(List<? extends ClassA> list) {
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}
//只能傳入ClassA的父類的容器
public void printList(List<? super ClassA> list) {
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}
除了上面的通配符"?",我們還可以直接用泛型方法去實(shí)現(xiàn)printListde,可以指定所有類型的列表或者ClassA的子類的列表:
public <T> void printList(List<T> list) {
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}
public <T extends ClassA> void printList(List<T> list) {
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}
當(dāng)然我們也能使用泛型的方式直接指定參數(shù)的上限,比如下面的foo方法就只能接收Number的子類:
public <T extends Number> void foo(T arg){
...
}
但是如果直接使用泛型的方式的話我們不能指定指定它的下限,例如下面兩種寫法都是不能通過編譯的:
//錯誤.不能直接指定泛型的下限
public <T super Number> void printList(List<T> list) {
...
}
//錯誤.不能直接指定泛型的下限
public <T super Number> void foo(T arg){
...
}
類型擦除
可能很多同學(xué)都會聽說過泛型類型擦除的概念,這個類型擦除具體指的是怎樣一回事?
可以看看下面的foo方法,它本來想實(shí)現(xiàn)的功能是:如果傳入的參數(shù)非空,就將它返回。否則,就創(chuàng)建一個同類型的實(shí)例返回。但是這段代碼是不能通過編譯的:
//錯誤,泛型的類型被擦除了,不能直接new出來
public <T> void foo(T arg) {
return arg != null ? arg : new T();
}
原因在于java的泛型實(shí)現(xiàn)中有個叫做類型擦除的機(jī)制。簡單來講就是運(yùn)行的時(shí)候是無法獲取到泛型使用的實(shí)際類型的。
例如上面的T類型,因?yàn)槲覀冊谶\(yùn)行時(shí)不能知道它到底是什么類型,所以也就無法將它new出來。
java代碼生成的Java字節(jié)代碼中是不包含泛型中的類型信息的,所有泛型類的類型參數(shù)在編譯時(shí)都會被擦除。虛擬機(jī)中沒有泛型,只有普通類和普通方法。因此泛型的類型安全是在編譯的時(shí)候去檢測的。
所以我們創(chuàng)建泛型對象時(shí)需要指明類型,讓編譯器盡早的做參數(shù)檢查。
像下面的代碼可以順利通過,甚至可以正常運(yùn)行,直到將獲取到的數(shù)值類型的數(shù)據(jù)強(qiáng)轉(zhuǎn)成字符串的時(shí)候才報(bào)ClassCastException異常:
List list = new ArrayList<String>();
list.add("abc");
list.add(123);
String elemt1 = (String) list.get(0);
String elemt2 = (String) list.get(1); // java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
我們可以用反射的方法的驗(yàn)證一下類型擦除:
List<Integer> list = new ArrayList<Integer>();
System.out.println("type : " + Arrays.toString(list.getClass().getTypeParameters()));
它得到的類型僅僅是一個占位符而已:
type : [E]
類型擦除機(jī)制的歷史原因
有人會問,為什么java會在編譯的時(shí)候?qū)㈩愋筒脸?而不像c++一樣通過在編譯的時(shí)候?qū)⒎盒皖悓?shí)例化為多個具體的類去實(shí)現(xiàn)泛型呢?
其實(shí)“實(shí)例化為多個具體的類”這樣的實(shí)現(xiàn)方式也是比較容易實(shí)現(xiàn)的,但是為了保持兼容性,所以java在泛型的實(shí)現(xiàn)上選取類型擦除的方式。實(shí)際上是做了一定的取舍的。
為什么說選用類型擦除是為了保持兼容性呢?因?yàn)榉盒筒⒉皇莏ava與生俱來的。實(shí)際上到了java5的時(shí)候才引入了泛型。
要讓以前編譯的程序在新版本的JRE還能正常運(yùn)行,就意味著以前沒有的限制不能突然冒出來。
例如在泛型出來之前java就已經(jīng)有了容器的存在,而且它具有可以存儲不同類型的的特性:
ArrayList things = new ArrayList();
things.add(Integer.valueOf(123));
things.add("abc");
那么這段代碼在Java 5引入泛型之后還必須要繼續(xù)可以運(yùn)行。
這里有兩種設(shè)計(jì)思路:
需要泛型化的類型(主要是容器(Collections)類型),以前有的就保持不變,然后平行地加一套泛型化版本的新類型;
直接把已有的類型泛型化,讓所有需要泛型化的已有類型都原地泛型化,不添加任何平行于已有類型的泛型版。
.NET在1.1 -> 2.0的時(shí)候選擇了上面選項(xiàng)的1,而Java則選擇了2。
從Java設(shè)計(jì)者的角度看,這個取舍很明白:.NET在1.1 -> 2.0的時(shí)候,實(shí)際的應(yīng)用代碼量還很少(相對Java來說),而且整個體系都在微軟的控制下,要做變更比較容易;
而在Java 1.4.2 -> 5.0的時(shí)候,Java已經(jīng)有大量程序部署在生產(chǎn)環(huán)境中,已經(jīng)有很多應(yīng)用和庫程序的代碼。如果這些代碼在新版本的Java中,為了使用Java的新功能(例如泛型)而必須做大量源碼層修改,那么新功能的普及速度就會大受影響,而且新功能會被吐槽。
在原地泛型化后,java.util.ArrayList這個類型變成了java.util.ArrayList<E>。但是以前的代碼直接用ArrayList,在新版本里必須還能繼續(xù)用,所以就引出了“raw type”的概念——一個類型雖然被泛型化了,但還可以把它當(dāng)作非泛型化的類型用。
ArrayList - raw type
ArrayList<E> - open generic type (assuming E is type variable)
ArrayList<String> - closed generic type
ArrayList<?> - unbounded wildcard type
下面這樣的代碼必須可以編譯運(yùn)行:
ArrayList<Integer> ilist = new ArrayList<Integer>();
ArrayList<String> slist = new ArrayList<String>();
ArrayList list; // raw type
list = ilist; // assigning closed generic type to raw type
list = slist; // ditto
所以java的設(shè)計(jì)者在考慮了這一點(diǎn)之后選用類型擦除也就顯而易見了。類型擦除實(shí)際上是將泛型類型轉(zhuǎn)換了Obejct。由于所有的java類都是Object的子類,所以實(shí)現(xiàn)起來就很簡單了。只需要在編譯的時(shí)候?qū)⑺械姆盒驼嘉环紦Q成Object就可以了:
//源碼的泛型代碼
public <T> void foo(T arg){
...
}
//編譯時(shí)轉(zhuǎn)換成的代碼
public void foo(Object arg){
...
}
而在擦除類型的同時(shí),java編譯器會對該方法的調(diào)用進(jìn)行類型檢查,防止非法類型的調(diào)用。
但如果在編寫代碼的時(shí)候就已經(jīng)用raw type的話,編譯器就不會做類型的安全性檢查了。
這樣的實(shí)現(xiàn)導(dǎo)致了一個問題,List<E>泛型參數(shù)E被擦除后就變成了Object,那么就不能在泛型中使用int、long等原生數(shù)據(jù)類型了,因?yàn)樗鼈儾⒉皇荗bject的子類。
據(jù)說當(dāng)時(shí)設(shè)計(jì)java語言的程序員和產(chǎn)品經(jīng)理打了一架,并且在打贏之后成功勸服產(chǎn)品經(jīng)理在提出兼容性這樣奇葩的需求之后做出一點(diǎn)小小的讓步。(雖然只是我胡說八道的腦補(bǔ),但誰知道當(dāng)時(shí)的實(shí)際情形是不是這樣的呢?)
于是乎我們現(xiàn)在在泛型中只能使用Integer、Long等封箱類型而不能用int、long等原生類型了。
ps: 上面這段類型擦除機(jī)制的歷史原因參考了RednaxelaFX大神知乎上的一個回答,有興趣的同學(xué)可以去知乎看看原來的完整回答