App下載

什么是泛型?Java基礎(chǔ)之泛型詳細(xì)知識(shí)點(diǎn)總結(jié)

猿友 2021-08-06 10:14:40 瀏覽數(shù) (5408)
反饋

一、什么是泛型?為什么要使用泛型?

泛型,即“參數(shù)化類型”。一提到參數(shù),最熟悉的就是定義方法時(shí)有形參,然后調(diào)用此方法時(shí)傳遞實(shí)參。那么參數(shù)化類型怎么理解呢?顧名思義,就是將類型由原來的具體的類型參數(shù)化,類似于方法中的變量參數(shù),此時(shí)類型也定義成參數(shù)形式(可以稱之為類型形參),然后在使用/調(diào)用時(shí)傳入具體的類型(類型實(shí)參)。

泛型的本質(zhì)是為了參數(shù)化類型(在不創(chuàng)建新的類型的情況下,通過泛型指定的不同類型來控制形參具體限制的類型)。也就是說在泛型使用過程中,操作的數(shù)據(jù)類型被指定為一個(gè)參數(shù),這種參數(shù)類型可以用在類、接口和方法中,分別被稱為泛型類、泛型接口、泛型方法。

沒有泛型之前:

private static void genericTest() {
    List arrayList = new ArrayList();
    arrayList.add("總有刁民想害朕");
    arrayList.add(7);
 
    for (int i = 0; i < arrayList.size(); i++) {
        Object item = arrayList.get(i);
        if (item instanceof String) {
            String str = (String) item;
            System.out.println("泛型測(cè)試 item = " + str);
        }else if (item instanceof Integer)
        {
            Integer inte = (Integer) item;
            System.out.println("泛型測(cè)試 item = " + inte);
        }
    }
}

如上代碼所示,在沒有泛型之前 類型的檢查 和 類型的強(qiáng)轉(zhuǎn) 都必須由我們程序員自己負(fù)責(zé),一旦我們犯了錯(cuò),就是一個(gè)運(yùn)行時(shí)崩潰等著我們。 

有了泛型之后:

private static void genericTest2() {
     List<String> arrayList = new ArrayList<>();
     arrayList.add("總有刁民想害朕");
     arrayList.add(7); //..(參數(shù)不匹配:int 無法轉(zhuǎn)換為String)
     ...
 }

如上代碼,編譯器在編譯時(shí)期即可完成 類型檢查 工作,并提出錯(cuò)誤(其實(shí)IDE在代碼編輯過程中已經(jīng)報(bào)紅了)

 二、泛型的特性是什么?

大家都知道,Java的泛型是偽泛型,這是因?yàn)镴ava在編譯期間,所有的泛型信息都會(huì)被擦掉,正確理解泛型概念的首要前提是理解類型擦除。Java的泛型基本上都是在編譯器這個(gè)層次上實(shí)現(xiàn)的,在生成的字節(jié)碼中是不包含泛型中的類型信息的,使用泛型的時(shí)候加上類型參數(shù),在編譯器編譯的時(shí)候會(huì)去掉,這個(gè)過程成為類型擦除。

如在代碼中定義List<Object>List<String>等類型,在編譯后都會(huì)變成List,JVM看到的只是List,而由泛型附加的類型信息對(duì)JVM是看不到的。Java編譯器會(huì)在編譯時(shí)盡可能的發(fā)現(xiàn)可能出錯(cuò)的地方,但是仍然無法在運(yùn)行時(shí)刻出現(xiàn)的類型轉(zhuǎn)換異常的情況,類型擦除也是Java的泛型與++模板機(jī)制實(shí)現(xiàn)方式之間的重要區(qū)別。 

什么是類型擦除?

類型擦除指的是通過類型參數(shù)合并,將泛型類型實(shí)例關(guān)聯(lián)到同一份字節(jié)碼上。編譯器只為泛型類型生成一份字節(jié)碼,并將其實(shí)例關(guān)聯(lián)到這份字節(jié)碼上。類型擦除的關(guān)鍵在于從泛型類型中清除類型參數(shù)的相關(guān)信息,并且再必要的時(shí)候添加類型檢查和類型轉(zhuǎn)換的方法。 類型擦除可以簡單的理解為將泛型java代碼轉(zhuǎn)換為普通java代碼,只不過編譯器更直接點(diǎn),將泛型java代碼直接轉(zhuǎn)換成普通java字節(jié)碼。 類型擦除的主要過程如下: 1.將所有的泛型參數(shù)用其最左邊界(最頂級(jí)的父類型)類型替換。 2.移除所有的類型參數(shù)。

通過兩個(gè)例子來理解泛型的類型擦除。

例一:

public class Test {
 
    public static void main(String[] args) {
 
        ArrayList<String> list1 = new ArrayList<String>();
        list1.add("abc");
 
        ArrayList<Integer> list2 = new ArrayList<Integer>();
        list2.add(123);
 
        System.out.println(list1.getClass() == list2.getClass());
    }
 
}

在這個(gè)例子中,我們定義了兩個(gè)ArrayList數(shù)組,不過一個(gè)是ArrayList<String>泛型類型的,只能存儲(chǔ)字符串;一個(gè)是ArrayList<Integer>泛型類型的,只能存儲(chǔ)整數(shù),最后,我們通過list1對(duì)象和list2對(duì)象的getClass()方法獲取他們的類的信息,最后發(fā)現(xiàn)結(jié)果為true。說明泛型類型StringInteger都被擦除掉了,只剩下原始類型。

例二:通過反射添加其它類型元素 

public class Test {
 
    public static void main(String[] args) throws Exception {
 
        ArrayList<Integer> list = new ArrayList<Integer>();
 
        list.add(1);  //這樣調(diào)用 add 方法只能存儲(chǔ)整形,因?yàn)榉盒皖愋偷膶?shí)例為 Integer
 
        list.getClass().getMethod("add", Object.class).invoke(list, "asd");
 
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }
    }
 
}

在程序中定義了一個(gè)ArrayList泛型類型實(shí)例化為Integer對(duì)象,如果直接調(diào)用add()方法,那么只能存儲(chǔ)整數(shù)數(shù)據(jù),不過當(dāng)我們利用反射調(diào)用add()方法的時(shí)候,卻可以存儲(chǔ)字符串,這說明了Integer泛型實(shí)例在編譯之后被擦除掉了,只保留了原始類型。

類型擦除后保留的原始類型,那么什么是原始類型呢?

原始類型 就是擦除去了泛型信息,最后在字節(jié)碼中的類型變量的真正類型,無論何時(shí)定義一個(gè)泛型,相應(yīng)的原始類型都會(huì)被自動(dòng)提供,類型變量擦除,并使用其限定類型(無限定的變量用Object)替換。

例三:原始類型Object

class Pair<T> {  
    private T value;  
    public T getValue() {  
        return value;  
    }  
    public void setValue(T  value) {  
        this.value = value;  
    }  
}

Pair的原始類型為:

class Pair {  
    private Object value;  
    public Object getValue() {  
        return value;  
    }  
    public void setValue(Object  value) {  
        this.value = value;  
    }  
}

因?yàn)樵?code>Pair<T>中,T 是一個(gè)無限定的類型變量,所以用Object替換,其結(jié)果就是一個(gè)普通的類,如同泛型加入Java語言之前的已經(jīng)實(shí)現(xiàn)的樣子。在程序中可以包含不同類型的Pair,如Pair<String>Pair<Integer>,但是擦除類型后他們的就成為原始的Pair類型了,原始類型都是Object。

從上面的例2中,我們也可以明白ArrayList<Integer>被擦除類型后,原始類型也變?yōu)?code>Object,所以通過反射我們就可以存儲(chǔ)字符串了。

如果類型變量有限定,那么原始類型就用第一個(gè)邊界的類型變量類替換。

比如: Pair這樣聲明的話:

public class Pair<T extends Comparable> {}

那么原始類型就是Comparable。

要區(qū)分原始類型和泛型變量的類型。

在調(diào)用泛型方法時(shí),可以指定泛型,也可以不指定泛型。

  • 在不指定泛型的情況下,泛型變量的類型為該方法中的幾種類型的同一父類的最小級(jí),直到Object
  • 在指定泛型的情況下,該方法的幾種類型必須是該泛型的實(shí)例的類型或者其子類
public class Test {  
    public static void main(String[] args) {  
 
        /**不指定泛型的時(shí)候*/  
        int i = Test.add(1, 2); //這兩個(gè)參數(shù)都是Integer,所以T為Integer類型  
        Number f = Test.add(1, 1.2); //這兩個(gè)參數(shù)一個(gè)是Integer,以風(fēng)格是Float,所以取同一父類的最小級(jí),為Number  
        Object o = Test.add(1, "asd"); //這兩個(gè)參數(shù)一個(gè)是Integer,以風(fēng)格是Float,所以取同一父類的最小級(jí),為Object  
 
        /**指定泛型的時(shí)候*/  
        int a = Test.<Integer>add(1, 2); //指定了Integer,所以只能為Integer類型或者其子類  
        int b = Test.<Integer>add(1, 2.2); //編譯錯(cuò)誤,指定了Integer,不能為Float  
        Number c = Test.<Number>add(1, 2.2); //指定為Number,所以可以為Integer和Float  
    }  
 
    //這是一個(gè)簡單的泛型方法  
    public static <T> T add(T x,T y){  
        return y;  
    }  
}

其實(shí)在泛型類中,不指定泛型的時(shí)候,也差不多,只不過這個(gè)時(shí)候的泛型為Object,就比如ArrayList中,如果不指定泛型,那么這個(gè)ArrayList可以存儲(chǔ)任意的對(duì)象。

三、泛型的使用方式 

泛型一般有三種使用方式:泛型類、泛型接口、泛型方法。

1.泛型類就是把泛型定義在類上,用戶使用該類的時(shí)候,才把類型明確下來 

這樣的話,用戶明確了什么類型,該類就代表著什么類型…用戶在使用的時(shí)候就不用擔(dān)心強(qiáng)轉(zhuǎn)的問題,運(yùn)行時(shí)轉(zhuǎn)換異常的問題了。

/**
 * Java泛型
 */
public class Demo {
    public static void main(String[] args) {
        // 定義泛型類 Test 的一個(gè)Integer版本
        Test<Integer> intOb = new Test<Integer>(88);
        intOb.showType();
        int i = intOb.getOb();
        System.out.println("value= " + i);
        System.out.println("----------------------------------");
        // 定義泛型類Test的一個(gè)String版本
        Test<String> strOb = new Test<String>("Hello Gen!");
        strOb.showType();
        String s = strOb.getOb();
        System.out.println("value= " + s);
    }
}
/*
使用T代表類型,無論何時(shí)都沒有比這更具體的類型來區(qū)分它。如果有多個(gè)類型參數(shù),我們可能使用字母表中T的臨近的字母,比如S。
*/
class Test<T> {
    private T ob;
 
    /*
    定義泛型成員變量,定義完類型參數(shù)后,可以在定義位置之后的方法的任意地方使用類型參數(shù),就像使用普通的類型一樣。
    注意,父類定義的類型參數(shù)不能被子類繼承。
    */
 
    //構(gòu)造函數(shù)
    public Test(T ob) {
        this.ob = ob;
    }
 
    //getter 方法
    public T getOb() {
        return ob;
    }
 
 
    //setter 方法
    public void setOb(T ob) {
        this.ob = ob;
    }
 
    public void showType() {
        System.out.println("T的實(shí)際類型是: " + ob.getClass().getName());
    }
}
 
/* output
    T的實(shí)際類型是: java.lang.Integer
    value= 88
    ----------------------------------
    T的實(shí)際類型是: java.lang.String
    value= Hello Gen!
*/

2.泛型接口

public interface Generator<T> {
    public T method();
}

實(shí)現(xiàn)泛型接口,不指定類型:

class GeneratorImpl<T> implements Generator<T>{
    @Override
    public T method() {
        return null;
    }
}

實(shí)現(xiàn)泛型接口,指定類型:

class GeneratorImpl<T> implements Generator<String>{
    @Override
    public String method() {
        return "hello";
    }
}

泛型方法:判斷一個(gè)方法是否是泛型方法關(guān)鍵看方法返回值前面有沒有使用 <> 標(biāo)記的類型,有就是,沒有就不是

public class Normal {
  // 成員泛型方法
  public <E> String getString(E e) {
  return e.toString();
  }
  // 靜態(tài)泛型方法
  public static <V> void printString(V v) {
  System.out.println(v.toString());
  }
 }
 // 泛型類中的泛型方法
 public class Generics<T> {
  // 成員泛型方法
  public <E> String getString(E e) {
  return e.toString();
  }
  // 靜態(tài)泛型方法
  public static <V> void printString(V v) {
  System.out.println(v.toString());
  }
 }

四、Java中的泛型通配符

常用的 T,E,K,V,?

本質(zhì)上這些個(gè)都是通配符,沒啥區(qū)別,只不過是編碼時(shí)的一種約定俗成的東西。比如上述代碼中的 T ,我們可以換成 A-Z 之間的任何一個(gè) 字母都可以,并不會(huì)影響程序的正常運(yùn)行,但是如果換成其他的字母代替 T ,在可讀性上可能會(huì)弱一些。通常情況下,T,E,K,V,? 是這樣約定的:

  • ? 表示不確定的 java 類型
  • T (type) 表示具體的一個(gè)java類型
  • K V (key value) 分別代表java鍵值中的Key Value
  • E (element) 代表Element

? 無界通配符:

我有一個(gè)父類 Animal 和幾個(gè)子類,如狗、貓等,現(xiàn)在我需要一個(gè)動(dòng)物的列表,我的第一個(gè)想法是像這樣的:

List<Animal> listAnimals

但是老板的想法確實(shí)這樣的:

List<? extends Animal> listAnimals

為什么要使用通配符而不是簡單的泛型呢?通配符其實(shí)在聲明局部變量時(shí)是沒有什么意義的,但是當(dāng)你為一個(gè)方法聲明一個(gè)參數(shù)時(shí),它是非常重要的。

static int countLegs (List<? extends Animal > animals ) {
    int retVal = 0;
    for ( Animal animal : animals )
    {
        retVal += animal.countLegs();
    }
    return retVal;
}
 
static int countLegs1 (List< Animal > animals ){
    int retVal = 0;
    for ( Animal animal : animals )
    {
        retVal += animal.countLegs();
    }
    return retVal;
}
 
public static void main(String[] args) {
    List<Dog> dogs = new ArrayList<>();
 	// 不會(huì)報(bào)錯(cuò)
    countLegs( dogs );
	// 報(bào)錯(cuò)
    countLegs1(dogs);
}

當(dāng)調(diào)用 countLegs1 時(shí),就會(huì)飄紅,提示的錯(cuò)誤信息如下:

所以,對(duì)于不確定或者不關(guān)心實(shí)際要操作的類型,可以使用無限制通配符(尖括號(hào)里一個(gè)問號(hào),即 <?> ),表示可以持有任何類型。像 countLegs 方法中,限定了上屆,但是不關(guān)心具體類型是什么,所以對(duì)于傳入的 Animal 的所有子類都可以支持,并且不會(huì)報(bào)錯(cuò)。而 countLegs1 就不行。

上界通配符 < ? extends E>:

上屆:用 extends 關(guān)鍵字聲明,表示參數(shù)化的類型可能是所指定的類型,或者是此類型的子類。

在類型參數(shù)中使用 extends 表示這個(gè)泛型中的參數(shù)必須是 E 或者 E 的子類,這樣有兩個(gè)好處:

如果傳入的類型不是 E 或者 E 的子類,編譯不成功泛型中可以使用 E 的方法,要不然還得強(qiáng)轉(zhuǎn)成 E 才能使用

下界通配符 < ? super E>:

下界: 用 super 進(jìn)行聲明,表示參數(shù)化的類型可能是所指定的類型,或者是此類型的父類型,直至 Object

到此本篇關(guān)于Java基礎(chǔ)之泛型的詳細(xì)知識(shí)點(diǎn)總結(jié)的文章就介紹到這了,想要了解更多相關(guān)Java泛型的詳細(xì)內(nèi)容,請(qǐng)搜索W3Cschool以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持!

0 人點(diǎn)贊