想要學(xué)好java,泛型機(jī)制是一個(gè)必須要掌握的知識(shí)點(diǎn),無(wú)奈很多書上寫的不是很啰嗦,就是概念太多難以理解,因此參考了很多篇文章,對(duì)其進(jìn)行整理了一下,希望對(duì)你有所幫助。
一、認(rèn)識(shí)泛型
1、為什么要引入泛型?
泛型其實(shí)是在jdk1.5中才添加的。在jdk1.5之前我們要?jiǎng)?chuàng)建一個(gè)容器對(duì)象,是這樣往里面添加內(nèi)容的。
List list = new ArrayList();
list.add("我是字符串");//可以添加字符串
list.add(10.67);//可以添加float
list.add(false);//可以添加boolean
也就是說我們創(chuàng)建了一個(gè)容器之后,我們可以往里面添加任何東西,這時(shí)候就麻煩了,如果我們只想保存字符串,但是一不小心存了一個(gè)int類型的值,在輸出的時(shí)候肯定會(huì)報(bào)錯(cuò)誤的。那怎么辦呢?于是乎,在jdk1.5添加了泛型機(jī)制,去規(guī)范我們輸入的值。
List<String> list = new ArrayList<String>();
這時(shí)候我們的list就只能保存String類型的值了,如果我們保存了int類型的值,那么就會(huì)在編譯期報(bào)錯(cuò)(一般情況下在ide寫代碼的時(shí)候,就會(huì)自動(dòng)編譯)。
2、泛型概念
有了上面這個(gè)例子,我們?cè)賮砝斫庖幌路盒偷母拍睿?/p>
泛型實(shí)現(xiàn)了了參數(shù)化類型的概念,使得代碼可以應(yīng)用于多種類型。
那什么是參數(shù)化類型呢?也就是說把我們要操作的數(shù)據(jù)類型保存為一個(gè)參數(shù)。比如下面這樣的
List<E>, Queue<E>
我們把要操作的數(shù)據(jù)類型變成了一個(gè)“E”。這個(gè)E就是一個(gè)類型參數(shù),我們可以指定E是具體String類型,也可以指定一個(gè)通配符,表示可以操作一類數(shù)據(jù)類型。
3、使用泛型的優(yōu)點(diǎn)
在java中,官方強(qiáng)烈推薦我們使用泛型。就是因?yàn)樗泻芏鄡?yōu)點(diǎn)。
(1)類型安全:我們?cè)谑褂梅盒椭?,可以指定輸入的類型,比如只能輸入String類型的值,輸入其他的就會(huì)報(bào)錯(cuò),這在代碼編寫時(shí),為我們提供了極大的方便。
(2)消除強(qiáng)制類型轉(zhuǎn)換:也就是說我們不需要進(jìn)行類型轉(zhuǎn)化,直接存儲(chǔ)、直接輸出。
(3)只在編譯器有效:也就是說在運(yùn)行時(shí)泛型是無(wú)效的。這避免了jvm花費(fèi)時(shí)間在運(yùn)行時(shí)做額外的操作。
對(duì)于第三點(diǎn),我們這里去驗(yàn)證一下(這里使用到了最基本的反射方法):
public class Test {
public static void main(String[] args) throws Exception {
//第一個(gè)list1我們只創(chuàng)建了一個(gè)容器:可以輸入任何類型
ArrayList list1=new ArrayList();
//第二個(gè)list2我們創(chuàng)建了一個(gè)泛型:只能輸入String類型
ArrayList<String> list2=new ArrayList<String>();
//使用反射機(jī)制,獲取Class
Class c1=list1.getClass();
Class c2=list2.getClass();
//疑問:在運(yùn)行時(shí),他們倆相等嘛?
System.out.print(c1==c2);
}
}
在第三點(diǎn)其實(shí)已經(jīng)給出答案了,輸出肯定是true。因?yàn)榉盒椭辉诰幾g器有效,在運(yùn)行時(shí)期無(wú)效,也就變成了一樣的。就好比,在編譯時(shí)期一個(gè)是羊,一個(gè)是披著狼皮的羊,在外表看著不一樣。在運(yùn)行時(shí)期,把狼皮脫掉了。就全暴露了,就都是羊了。
目前為止,我們已經(jīng)把泛型的產(chǎn)生的原因(這只是原因之一),泛型的概念以及泛型的優(yōu)點(diǎn)說出來了,下面我們就來看看,泛型機(jī)制在java中是如何使用的。
二、泛型的使用
泛型的使用主要是在三個(gè)方面,泛型類、泛型接口、泛型方法。我們一個(gè)一個(gè)去看。
1、泛型類
泛型類的使用也是非常簡(jiǎn)單的,和普通類的區(qū)別就是類名后有類型參數(shù)列表 <E>,既然是類型參數(shù)列表,也就是說可以有多個(gè)類型參數(shù),比如<E,T>。我們直接創(chuàng)建一個(gè)泛型類看看吧。
//這里的E和T,可以有任意多個(gè),名字使我們自己定的
public class Generic<E,T>{
//這里的E和T由外部指定
private T key;
private E e;
public Generic(E e,T key) {
this.e=e;
this.key = key;
}
//我們使用E和T就像使用String這些一樣
public T getKey(){
return key;
}
public E getE(){
return e;
}
}
我們會(huì)發(fā)現(xiàn),其實(shí)泛型類和普通類的區(qū)別也就是有了一個(gè)參數(shù)類型列表:Generic<E,T>。這里的<E,T>我們還可以添加任意多個(gè)。他就像String,Integer等等類型一樣。名字是我們?nèi)〉?。使用的時(shí)候,也是和String、Integer這些一樣。
下面我們就使用一下這個(gè)泛型類
public class Test {
public static void main(String[] args) {
Generic<Integer,String> generic=new Generic<Integer,String>(123, "test");
System.out.println(generic.getE());
System.out.println(generic.getKey());
}
}
//輸出:
//test
//123
在使用這個(gè)泛型類的時(shí)候,有幾個(gè)地方需要我們?nèi)プ⒁猓?/p>
(1)實(shí)例化泛型類時(shí),必須指定E和T的具體類型,比如這里指定的是Integer和String
(2)指定的具體類型必須是類,不能是int,float等這些基礎(chǔ)類型
(3)不能對(duì)泛型類使用instanceof。為什么呢?這是因?yàn)榉盒皖愔辉诰幾g期有效,在運(yùn)行時(shí)期不區(qū)分是什么類型,也就是在上面說的,穿著狼皮的羊脫掉狼皮之后,兩只羊就都一樣了。比如下面的代碼是不合法的。
User<Integer> integerUser = new User<Integer>();
if(integerUser instanceof User<Integer>){ }
//會(huì)出現(xiàn)以下錯(cuò)誤提示
//Cannot perform instanceof check against parameterized type Box<Integer>.
//Use the form Box<?> instead since further
//generic type information will be erased at runtime
2、泛型接口
泛型接口其實(shí)和泛型類一樣,和普通接口的區(qū)別也是后面添加了類型參數(shù)列表 <E>。我們先創(chuàng)建一個(gè)泛型接口來看看。
public interface GenericInterface<T> {
//定義一個(gè)普通方法:參數(shù)是E和T
//注意:這可不是泛型方法
public void test(T t) ;
}
注意:在泛型接口里面我們只是定義了一個(gè)普通的方法,可不是泛型方法,然后我們就可以使用一般的接口那樣使用泛型接口了。
//GenericInterface<String>需要指定具體的類型String
public class GenericTest implements GenericInterface<String>{
//泛型接口中
@Override
public void test(String name) {
System.out.println("具體類型是:String:"+name);
}
public static void main(String[] args) {
GenericTest genericTest = new GenericTest();
genericTest.test("泛型接口");
}
}
在使用泛型接口時(shí)候和使用泛型類一樣同樣有幾個(gè)點(diǎn)需要我們知道:
(1)繼承泛型接口的時(shí)候就需要指定具體是什么類型
(2)泛型中的方法也需要對(duì)相應(yīng)的泛型參數(shù)賦予具體的類型。
3、泛型方法
泛型方法是什么意思呢?也就是我們輸入?yún)?shù)的時(shí)候,輸入的是泛型參數(shù),而不是具體的參數(shù)。我們?cè)谡{(diào)用這個(gè)泛型方法的時(shí)候,需要對(duì)泛型參數(shù)實(shí)例化。我們還是直接看例子:
//定義了一個(gè)泛型方法
public <T> T genericMethod(T t){
return t;
}
這里最重要的就是public后面的<T>,只有有了這個(gè)東西才稱得上泛型方法。當(dāng)然這里的<T>也是一個(gè)泛型化列表??梢允?lt;E,T等等>。我們給出幾個(gè)普通方法,對(duì)比一下區(qū)別所在:
//1、public后面沒有<T>
public T getName(T t){
return t;
}
//2、就是和普通方法一樣
public String getName(String b) {
return b;
}
//3、錯(cuò)誤的泛型方法
public <T> T getName(Generic<E> e){
//錯(cuò)誤原因是因?yàn)镋未聲明,我們不知道
}
現(xiàn)在我們知道區(qū)別了吧,也就是說泛型方法的標(biāo)志就是,權(quán)限修飾符后面的<T>。我們看一下如何去使用。
public class GenericTest {
public static void main(String[] args) {
Generic genericTest = new Generic();
String a=genericTest.genericMethod("這里可以是任意類型");
int b=genericTest.genericMethod(123);
double c=genericTest.genericMethod(12.34);
}
}
我們可以像普通方法那樣去使用即可。
注意:在靜態(tài)方法中使用泛型參數(shù)的時(shí)候,需要我們把靜態(tài)方法定義為泛型方法
//比如說:我們想在靜態(tài)方法getName中使用泛型參數(shù)T
public static void getName(T t){
//這種是錯(cuò)誤的,我們需要把靜態(tài)方法轉(zhuǎn)變成泛型方法。
}
public static <T> void getName(T t){
//這樣就可以了
}
4、泛型通配符
其實(shí)泛型通配符嚴(yán)格的劃分是屬于泛型類一部分的,為什么要用到泛型通配符呢?因?yàn)橛袝r(shí)候我們希望傳入的類型在一個(gè)指定的范圍內(nèi)。舉個(gè)例子,之前我們傳入的類型必須指定為Integer類型的,但是后來業(yè)務(wù)變了,Integer的父類Number類也可以傳入。這時(shí)候就需要用到泛型通配符了。
泛型中有三種通配符形式:
(1)<?> 無(wú)限制通配符:表示我們可以傳入任意類型的參數(shù)
(2)<? extends E> 表示類型的上界是E,只能是E或者是E的子孫類。
(3)<? super E> 聲明了類型的下界E,只能是E或者是E的父類。
我們使用代碼舉個(gè)例子相信你就會(huì)明白了。
//在這里我們傳入Number或者是Number的子類都可以
private <T extends Number> T getName(T t){
return t;
}
//在這里我們傳入E或者是E的父類都可以
private <E> E add(List<? super E> e){
return e;
}
5、類型擦除
我們?cè)谖恼乱婚_始就曾經(jīng)說過,泛型只在編譯期有效,在運(yùn)行期虛擬機(jī)是分辨不出來的,而且我們還用反射機(jī)制來驗(yàn)證了一下,發(fā)現(xiàn)在運(yùn)行期兩個(gè)ArrayList確實(shí)是一樣的。那么問題來了,從編譯期能夠識(shí)別泛型,再到運(yùn)行期不能識(shí)別泛型肯定需要一個(gè)過程,在這個(gè)過程中編譯器肯定要對(duì)泛型進(jìn)行一個(gè)處理,才能到運(yùn)行期。這個(gè)處理就是類型擦除。
也就是說,在編譯時(shí)期java編譯器就完成了類型擦除。我們可以先看下面一種情況:

上面我們定義了這兩個(gè)代碼會(huì)出現(xiàn)這樣的問題,這是因?yàn)閖ava編譯器在編譯時(shí)期就進(jìn)行了類型擦除,擦出了之后發(fā)現(xiàn)兩個(gè)方法的方法名、參數(shù)列表一樣。于是出現(xiàn)了兩個(gè)一樣的方法,報(bào)了這個(gè)錯(cuò)誤。
上面出現(xiàn)的這種情況對(duì)我們來說真的是太麻煩了,如何解決這個(gè)問題呢?java又為我們提供了一個(gè)機(jī)制:邊界,來解決這個(gè)問題。什么意思呢?之前我們的類型擦除,都是直接擦除到Object,現(xiàn)在有了邊界之后,我們只擦出到一定的界限就不擦出了。我們?cè)賮砜聪旅娴氖褂昧诉吔缰蟮暮锰帲?/p>
public class GenericTest {
interface A {
void testA();
}
interface B{
void testB();
}
public static class Test<T extends A & B>{
private T val;
public Test(T val){
val = val;
}
public void test(){
val.testA();
val.testB();
}
}
}
現(xiàn)在應(yīng)該能看明白了,我們限定了類型擦除的邊界之后,就不會(huì)出現(xiàn)這種錯(cuò)誤了。編譯器會(huì)把類型參數(shù)替換為第一個(gè)邊界。如果你還不明白,就動(dòng)手操作一遍。
三、泛型總結(jié)
如果我們之前了解過java中的語(yǔ)法糖的知識(shí)話,我們應(yīng)該知道其是泛型就是一個(gè)語(yǔ)法糖,語(yǔ)法糖就是一個(gè)方便程序員的功能,對(duì)語(yǔ)言沒有任何影響。真正想要掌握泛型機(jī)制的話,還需要自己動(dòng)手對(duì)每一塊內(nèi)容自己寫一遍。OK,泛型就先到這里。
