泛型是什么?
泛型的英文是 generics,generic 的意思是通用,而翻譯成中文,泛應(yīng)該意為廣泛,型即是類型。所以泛型就是能廣泛適用的類型。
但泛型還有一種較為準(zhǔn)確的說法就是為了參數(shù)化類型,或者說可以將類型當(dāng)作參數(shù)傳遞給一個類或者是方法。
那么,如何解釋類型參數(shù)化呢?
public class Test {
Object value;
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}
上面的Object為java.lang包中的所有類的根類,于是,我們可以這樣使用它。
Test test = new Test ();
test.setValue('x');//Character包裝類的自動裝箱
String value1 = (String) test.getValue();
test.setValue(19950904);//Integer包裝類自動裝箱
int value = (int) test.getValue();
Java八種基本數(shù)據(jù)類型對應(yīng)的包裝類,以下以int和char型數(shù)據(jù)對應(yīng)的包裝類為例:
Character ch = new Character('x');//等價于test.setValue('x');
test.setValue(ch )
Integer in = new Integer("19950904");//等價于test.setValue(19950904);
test.setValue(in )
上述方法簡單粗暴,只要我們做一個正確的類型強制轉(zhuǎn)換就OK了
但是通過在JDK1.5版本引入泛型之后,它給我們帶來了完全不一樣的編程體驗
public class Test<T> {
T value;
public Object getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
這就是泛型,它將Test類的成員變量value這個屬性的類型也參數(shù)化了,這就是所謂的參數(shù)化類型。以下是它的使用方法。
Test<Character> test1 = new Test<Character>();
test1 .setValue('x');
char value1 = test1 .getValue();
Test<Integer> test2 = new Test<Integer>();
test2.setValue(19950904);
int value2 = test2 .getValue();
上述代碼最顯而易見的好處就是不用再對取出來的結(jié)果進行強制的類型轉(zhuǎn)換了。除此之外,還有另外一個特點,那就是,泛型除了可以將參數(shù)類型化外,當(dāng)參數(shù)一旦確定好,如果參數(shù)類型不匹配,編譯器在編譯的時候就會因為參數(shù)類型不匹配而報錯不通過,如下代碼在校驗泛型指定數(shù)據(jù)類型是否匹配時就會報錯:
Test<String> test1 = new Test<String>();
test1.setValue('liupeng');
String value1 = test1 .getValue();
test1.setValue(19950904);//泛型指定類型不匹配導(dǎo)致報錯
因此,綜合上述信息,我們可以得出以下結(jié)論。
1、與通過Object根類來代替一切類型這樣簡單粗暴的方式而言,泛型的引入使得數(shù)據(jù)的類別可以像參數(shù)一樣由外部傳遞進來。它提供了一種擴展能力。它更符合面向抽象的編程思想。
2、當(dāng)具體的類型確定后,泛型又提供了一種類型檢測的機制,只有相匹配的數(shù)據(jù)才能正常的賦值,否則編譯器就不通過。所以說,它是一種類型安全檢測機制,一定程度上提高了軟件的安全性。
3、泛型提高了程序代碼的可讀性,不必要等到運行的時候才去進行數(shù)據(jù)類型的強制轉(zhuǎn)換,在定義或者實例化階段,因為泛型類型顯示話的效果,程序員能夠一目了然猜測出代碼要操作的數(shù)據(jù)類型。
接下來我們介紹下泛型的相關(guān)知識
泛型的分類,按照使用情況可以分為三類:泛型類、泛型方法和泛型接口
1、泛型類
泛型類的定義如下:
public class Test<T> {
private T field1;
}
尖括號 <> 中的 T 被稱作是類型參數(shù),用于指代任何類型。事實上,T只是一種習(xí)慣性寫法,如果你愿意。你可以寫成任意的字符串,如下:
public class Test<LiuPeng> {
private LiuPeng Field1;
}
但出于規(guī)范的目的,Java還是建議我們用單個大寫字母來代表類型參數(shù)。常見的如:
(1) T 代表一般的任何類。
(2) E 代表 Element 的意思,或者 Exception 異常的意思。
(3) K 代表 Key 的意思。
(4) V 代表 Value 的意思,通常與 K 一起配合使用。
(5) S 代表 Subtype 的意思,文章后面部分會講解示意。
以下是泛型類的使用:
Test<String> test1 = new Test<>();//泛型指定具體的顯示化數(shù)據(jù)類型為String
Test<Integer> test2 = new Test<>();//泛型指定具體的顯示化數(shù)據(jù)類型為Integer
注意:泛型指定的具體數(shù)據(jù)類型不能為基本數(shù)據(jù)類型,必須為其對應(yīng)的包裝類
在對泛型類創(chuàng)建實例對象的時候,只需要在尖括號中賦上具體的數(shù)據(jù)類型(可以是任何類)即可,T就會被替換成對應(yīng)的類型,如String 或者是 Integer,泛型類被創(chuàng)建具體實例對象的時候,內(nèi)部自動擴展成以下代碼:
public class Test<String>
String field1;
}
當(dāng)然,泛型類也可以同時接受多個類型參數(shù),如下定義所示:
public class MultiType <E,T>{
E filed1;
T filed2;
public E getFiled1(){
return filed1;
}
public T getFiled2(){
return filed2;
}
}
2、泛型方法
泛型方法的定義如下:
public class Test {
public <T> void testMethod(T t){
}
}
泛型方法與泛型類稍有不同的地方是,類型參數(shù)也就是尖括號那一部分是寫在返回值前面的。<T> 中的 T 被稱為類型參數(shù),而方法中參數(shù)中的 T 被稱為參數(shù)化類型,它不是運行時真正的參數(shù)。
當(dāng)然,聲明的類型參數(shù),其實也是可以當(dāng)作返回值的類型的。
public <T> T testMethod(T t){
return null;
}
泛型類與泛型方法是可以同時出現(xiàn)在一個類中的,如下所示:
public class Test<T>{
public void testMethod1(T t){
System.out.println(t.getClass().getName());//取的是類的全路徑名稱
}
public <T> T testMethod2(T t){
return t;
}
}
上面代碼中,Test<T> 是泛型類,testMethod1 是泛型類中的普通方法,而 testMethod2 是一個泛型方法。而泛型類中的類型參數(shù)與泛型方法中的類型參數(shù)是沒有相應(yīng)的聯(lián)系的,泛型方法始終以自己定義的類型參數(shù)為準(zhǔn)。
針對上面的代碼,我們可以這樣編寫測試代碼。
Test<String> t = new Test();
t.testMethod1("liupeng");
Integer i = t.testMethod2(new Integer(19950904));
泛型類的實際類型參數(shù)是 String,而傳遞給泛型方法的類型參數(shù)是 Integer,兩者互不相干。
但是,為了避免混淆,如果在一個泛型類中存在泛型方法,那么兩者的類型參數(shù)最好不要同名。比如,Test<T> 代碼最好寫成以下形式:
public class Test<T>{
public void testMethod1(T t){
System.out.println(t.getClass().getName());//取的是類的全路徑名稱
}
public <E> E testMethod2(E e){
return e;
}
}
3、泛型接口
泛型接口的定義如下:
public interface Interable<T> {
}
泛型接口與泛型類相似,可以在泛型接口中聲明相應(yīng)的泛型方法:
public interface Interable<T>{
public void testMethod1(T t);
public <E> E testMethod2(E e);
}
通配符 ?
除了用<T>表示泛型外,還有<?>這種形式。?被稱為通配符。
class Father{}
class Son extends Father{}
Son son = new Son ();
Father father = son;
如上代碼顯示,F(xiàn)ather是Son的父類,它們之間是繼承關(guān)系,所以Son的實例可以給一個 Father引用賦值,那么以下代碼會編譯通過嗎?
List<Son> sonList = new ArrayList<>();
List<Father> fatherList = sonList ;
答案是肯定不行的。
編譯器不會讓它通過的。Son是Father的子類,不代表 List<Son> 和 List<Father> 有繼承關(guān)系。
但是,現(xiàn)實的編碼中,確實存在此類需求,希望泛型能夠處理某一范圍內(nèi)的數(shù)據(jù)類型,比如某個類和它的子類,對此 Java 引入了通配符這個概念。
所以,通配符的出現(xiàn)是為了指定泛型中的類型范圍。
通配符有 下一三種形式。
1、 <?> 被稱作無限定的通配符。
無限定通配符
public void testMethod(Collection<?> collection){
}
上面的代碼中,方法內(nèi)的參數(shù)是被無限定通配符修飾的 Collection 對象,它隱略地表達了一個意圖或者可以說是限定,那就是 testMethod() 這個方法內(nèi)部無需關(guān)注 Collection 中的真實類型,因為它是未知的。所以,你只能調(diào)用 Collection 中與類型無關(guān)的方法。
pubilc class Tset{
public void testMethod(Collection<?> collection){
collection.add(1995);//編譯出錯,無法確認真實數(shù)據(jù)類型
collection.add("liupeng");//編譯出錯,無法確認真實數(shù)據(jù)類型
collection.add(new Object());//編譯出錯,無法確認真實數(shù)據(jù)類型-正常
collection.iterator().next();//迭代器遍歷,與數(shù)據(jù)類型無關(guān)
collection.size();//容器容量,與數(shù)據(jù)類型無關(guān)-正常
}
}
有人說,<?> 提供了只讀的功能,也就是它刪減了增加具體類型元素的能力,只保留與具體類型無關(guān)的功能。它不管裝載在這個容器內(nèi)的元素是什么類型,它只關(guān)心元素的數(shù)量、容器是否為空等。
2、 <? extends T> 被稱作有上限的通配符。
<?> 代表著類型未知,但是我們的確需要對于類型的描述再精確一點,我們希望在一個范圍內(nèi)確定類別,比如類型 A 及 類型 A 的子類都可以。
那么我們就可以采用如下的定義方式:
public void test(Collection<? extends Father> param){
}
上面代碼中,para 這個 Collection 接受 Father及 Father的子類的類型。
但是,它仍然喪失了寫操作的能力。也就是說以下代碼仍然編譯不通過。
param.add(new Son());
param.add(new Father());
沒有關(guān)系,我們不知道具體類型,但是我們至少清楚了類型的范圍。
3、 <? super T> 被稱作有下限的通配符。
這個和 <? extends T> 相對應(yīng),代表 T 及 T 的超類。
public void test(Collection<? super Son> param){
}
<? super T> 神奇的地方在于,它擁有一定程度的寫操作的能力。
public void test(Collection<? super Son> param){
param.add(new Son());//編譯通過
param.add(new Father());//編譯不通過
}
<類型擦除>
泛型是Java 1.5版本才引進的概念,在這之前是沒有泛型的概念的,但顯然,泛型代碼能夠很好地和之前版本的代碼很好地兼容。
這是因為,泛型信息只存在于代碼編譯階段,在進入JVM之前,與泛型相關(guān)的信息會被擦除掉,專業(yè)術(shù)語叫做類型擦除。
通俗地講,泛型類和普通類在java虛擬機內(nèi)是沒有什么特別的地方。
List<String> list1 = new ArrayList<String>();
List<Integer> list2 = new ArrayList<Integer>();
System.out.println(list1.getClass() == list.getClass());
輸出的結(jié)果為true是因為List<String>和 List<Integer>在jvm中的Class都是 List.class。泛型信息被擦除了。
有人可能會問,那類型String和Integer怎么辦?答案是泛型轉(zhuǎn)譯。
public class Test<T>{
T object;
public Test(T object) {//通過構(gòu)造方法給成員變量object傳遞值
this.object = object;
}
}
Test是一個泛型類,我們查看它在運行時的狀態(tài)信息可以通過Java反射機制。
Test<String> test = new Test<String>("liupeng");
System.out.println("test class is:" + test.getClass().getName());
輸出的結(jié)果是:
test class is:com.hundsun.lp.Test
Class 的類型仍然是Test并不是Test<T>這種形式,那我們再看看泛型類中T的類型在 jvm 中是什么具體類型。
Field[] fd = test.getClass().getDeclaredFields();
for (Field f:fd) {
System.out.println("Field name "+f.getName()+" type:"+f.getType().getName());
}
輸出結(jié)果是
Field name object type:java.lang.Object
我們更改下上述代碼,再看下測試結(jié)果
public class Test<T extends String>{
// public class Test<T>{
T object;
public Erasure(T object) {
this.object = object;
}
}
輸出結(jié)果是
Field name object type:java.lang.String
綜上所述,我們可以得出以下結(jié)論:
在泛型類被類型擦除的時候,之前泛型類中的類型參數(shù)部分如果沒有指定上限,如<T>則會被轉(zhuǎn)譯成普通的Object 類型,如果指定了上限如<T extends String>則類型參數(shù)就被替換成類型上限。
所以,在反射中。
public class Test<T>{
T object;
public Test(T object) {
this.object = object;
}
public void add(T object){
}
}
add() 這個方法對應(yīng)的Method的簽名應(yīng)該是 Object.class。
實際結(jié)果如下:
Test <String> test = new Test <String>("hello");
System.out.println("erasure class is:"+ test.getClass().getName());
Method[] methods = test.getClass().getDeclaredMethods();
for ( Method m:methods ){
System.out.println(" method:"+m.toString());
}
輸出結(jié)果是:
method:public void com.hundsun.lp.Test.add(java.lang.Object)
也就是說,如果你要在反射中找到add對應(yīng)的 Method,你應(yīng)該調(diào)用 getDeclaredMethod("add",Object.class) 否則程序會報錯,提示沒有這么一個方法,原因就是類型擦除的時候,T被替換成Object 類型了。
類型擦除帶來的局限性
類型擦除,是泛型能夠與之前的java版本代碼兼容共存的原因。但也因為類型擦除,它會抹掉很多繼承相關(guān)的特性,這是它帶來的局限性。
public class Test{
public static void mian(){
List<Integer> list1 = new ArrayList<>();
list1.add(19950904);//正常
list1.add("liupeng");//類型不匹配導(dǎo)致報錯
}
}
正常情況下,因為泛型的限制,編譯器不讓最后一行代碼編譯通過,因為類似不匹配,但是,基于對類型擦除的了解,利用反射,我們可以繞過這個限制。
對泛型方法的困惑
public <T> T test(T t){
return null;
}
有的同學(xué)可能對于連續(xù)的兩個T感到困惑,其實<T>是為了說明類型參數(shù),是聲明,而后面的不帶尖括號的T是方法的返回值類型。
你可以相像一下,如果test()這樣被調(diào)用
test("liupeng");
那么實際相當(dāng)于
public String test(String t);
Java 不能創(chuàng)建具體類型的泛型數(shù)組,因為類型擦除,程序無法分辨一個數(shù)組中的元素類型具體是什么類型。
List<Integer>[] li2 = new ArrayList<Integer>[];//List<Integer>在 jvm 中等同于List<Object>
List<Boolean> li3 = new ArrayList<Boolean>[];//List<Boolean>在 jvm 中等同于List<Object>
但是,
List<?>[] li3 = new ArrayList<?>[10];
li3[1] = new ArrayList<String>();
List<?> v = li3[1];
借助于無限定通配符卻可以,前面講過 ? 代表未知類型,所以它涉及的操作都基本上與類型無關(guān),因此 jvm 不需要針對它對類型作判斷,因此它能編譯通過,但是,只提供了數(shù)組中的元素,因為通配符原因,它只能讀,不能寫。比如,上面的 v 這個局部變量,它只能進行 get() 操作,不能進行 add() 操作。
最后,還是要建議大家使用泛型,如官方文檔所說的,如果可以使用泛型的地方,盡量使用泛型。
畢竟它抽離了數(shù)據(jù)類型與代碼邏輯,
本意是提高程序代碼的簡潔性和可讀性,
并提供可能的編譯時類型轉(zhuǎn)換安全檢測功能。