深入分析java中的泛型機(jī)制

想要學(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編譯器就完成了類型擦除。我們可以先看下面一種情況:

1-類型擦除.png

上面我們定義了這兩個(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,泛型就先到這里。


宣傳頁(yè)_副本2_副本.jpg
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容