聊聊 Java 泛型的使用

在剛開始接觸泛型時,我一直有個疑惑,就是爲何我不能爲靜態字段定義泛型類型,我也嘗試過,只是沒法經過編譯而且會獲得「沒法從靜態上下文中引用非靜態‘【類型變量 V」,這時我纔有一種模糊的意識,原來泛型信息是屬於對象的,而不是類。java

public class NoStaticFieldGenerics<V> {

    //  嘗試定義靜態泛型字段
    public static V staticGenericFiled;
    
    //  上面的錯誤信息和在靜態方法中嘗試反問非靜態字段或方法是同樣的
    public static void staticGetValue() {
        getValue();
    }

    private V value;

    public V getValue() {
        return value;
    }

    public void setValue(V value) {
        this.value = value;
    }
}
複製代碼

其實後來想了想很好理解,泛型信息的參數化或實例化基本上是在建立對象才指定的,一方面二者生命週期不一樣,另外一方面就算真的有這種靜態泛型字段,那我該聽誰的呢?由於我就這麼一份,而大家每一個具體對象中泛型類型又不相同,因此我認爲沒有靜態的泛型字段確實合情合理。數組

List<String> strList = new ArrayList<String>();
List<Integer> intList = new ArrayList< Integer >();
複製代碼

開頭先講了一個本身使用泛型的困惑,可是在實際開發中本身使用泛型可並不僅有這麼一個困惑,而是一個接着一個,這篇內容並不會深刻講解原理,而是講一些常見的使用和對應使用時的思考,而原理性的內容會放到下一篇着重介紹。安全

進入正題,下面咱們先來看看泛型的一些常見使用:bash

1.直接使用已有泛型:electron

List<String> strList = new ArrayList<String>();
strList.add("a");
//  下面這行是沒法經過編譯的,由於已經聲明瞭這個 List 的泛型類型是 String,因此沒法插入其餘類型
strList.add(123);
複製代碼

2.泛型類或接口:ide

當我在使用一個新東西的時候,若是我不知道怎麼使用它,那我會先去看看系統或者別人怎麼用它,因此這時我直接點開 List 的源碼,看到它的聲明是什麼樣的。ui

//  若是咱們不知道如何定義時,能夠看看系統或別人是如何作的
public interface List<E> extends Collection<E> {
  ...
}
//  這是咱們自定義的用來存放貨物的集裝箱
public interface Container<GOODS> {
    
    void put(GOODS goods);
}
複製代碼

固然這裏的 GOODS 能夠是任意的字符,常見的有 T、E、K、V 等,它其實只是一個使用的代號,儘可能不要和其餘已有類重名就行,哪怕真的亂來寫 String 也是能夠的,可是這樣話,想要使用和泛型類型名稱衝突的類就只能使用時寫對應類的全路徑了。this

3.繼承或實現已有泛型類或接口:spa

下面代碼中錯誤寫法看上去是我定義了一個裝電子產品的集裝箱,而且我也聲明瞭對應的泛型,可是爲何系統提示重寫方法時會生成一個參數類型是 Object 的方法?個人 EP 類型呢?這種其實錯誤用法,忘了 Container 自己也有泛型,咱們只是在 ElectronicProductContainer 上面聲明瞭泛型,並無將其應用到 Container 中,因此這時能夠先擡頭看看上面 List 是如何聲明的。code

若是說正確寫法的話,並不只限於 List 的那一種,那種的靈活性更大,我能夠寫代碼的時候動態指定我想要使用的類型,好比我想裝筆記本電腦,或是裝電視進去,固然我也直接指定此次的泛型是什麼,就像寫法二那樣直接指定咱們要使用類型。

但無論怎麼寫,若是咱們要繼承或實現已有的泛型,那麼就必須在父類或接口邊上聲明此次要使用的泛型類型是什麼,這樣纔不會讓泛型信息丟失。

//  錯誤寫法,裝電子產品的集裝箱
public class ElectronicProductContainer<EP> implements Container {
    
    @Override
    public void put(Object o) {
        
    }
}

//  正確寫法一:
public class ElectronicProductContainer<EP> implements Container<EP> {

    @Override
    public void put(EP ep) {
        
    }
}

//  正確寫法二:
public class PhoneContainer extends ElectronicProductContainer<Phone> {

    @Override
    public void put(Phone phone) {

    }
}

public abstract class ElectronicProductContainer<EP> implements Container<EP> { }
public class Phone { ... }
複製代碼

4.泛型約束其一:

先來看看沒有任何約束的狀況:

//  這是一開始的咱們剛剛聲明好的泛型類,咱們但願這個泛型類裏面只能存放電子相關的類型
public class ElectronicProductContainer<EP> implements Container<EP> {

    @Override
    public void put(EP ep) {
        //  這裏的 ep 只能調用 Object 相關的方法
    }
}

//  前兩個的使用狀況是咱們期待的,放手機和 PC 的集裝箱
ElectronicProductContainer<Phone> phoneEPContainer = new ElectronicProductContainer<>();
ElectronicProductContainer<PersonalComputer> pcEPContainer = new ElectronicProductContainer<>();
//  不當心亂入了軍火???是誰再亂來???想要搞死我???
//  有內鬼終止交易
ElectronicProductContainer<Weapon> weaponEPContainer = new ElectronicProductContainer<>();
複製代碼

因此有時咱們但願對聲明的泛型作出一些約束,好比只能放某些類型的數據,或但願能夠調到它一些比較具體的方法,而不僅有 Object 的方法,就像上面例子的中的代碼,當咱們試圖調用 put(EP ep) 裏參數 ep 的方法時,發現只有 Object 方法,這其實也對,由於泛型信息在運行時被擦除了,因此並不知道確切的類型信息,爲了能讓咱們這麼方便的使用其實也是由於編譯器幫咱們作了不少處理,這裏不細說,先來看看,若是想要約束存放類型或想要調用某個類或接口的方法時,該如何來處理,具體例子以下:

//  對於上面的類我要作出一些微小調整
//  把 <EP> 改成了 <EP extends ElectronicProduct>
public class ElectronicProductContainer<EP extends ElectronicProduct> implements Container<EP> {

    @Override
    public void put(EP ep) {
        //  這裏也能夠調用 ElectronicProduct 的方法了
        System.out.println(ep.productName());
        System.out.println(ep.productPrice());
    }
}

public interface ElectronicProduct {

    String productName();
    String productDate();
    float productPrice();
}

//  前面兩行依然正常,由於 Phone 和 PersonalComputer 都實現了 ElectronicProduct 接口
ElectronicProductContainer<Phone> phoneEPContainer = new ElectronicProductContainer<>();
ElectronicProductContainer<PersonalComputer> pcEPContainer = new ElectronicProductContainer<>();
//  第三行無法經過編譯,由於並不符合咱們聲明的約束條件
//  除非咱們中出了臥底,把約束條件悄悄告訴了敵人
ElectronicProductContainer<Weapon> weaponEPContainer = new ElectronicProductContainer<>();
複製代碼

能夠理解爲我對 EP 這個泛型類型作了約束,我告訴編譯器它是什麼,這個泛型能夠被實例化成什麼,這裏的 EP 只能被實例化成 ElectronicProduct 以及它的子類,固然,extends 後面能夠跟着接口也能夠跟着類,而且還能夠同時存在多個不一樣約束,不過本着 Java 類單繼承的特性,因此 extends 後面只能跟一個類,不過接口能夠跟多個,若是有多個約束,要用 & 隔開,就像這樣 T extends A & B & C。

5.泛型約束其二:

再講下面類型約束前,先來說講數組,《Effective Java》中講到數組與泛型兩個重要的不一樣,一個是數組是協變類型的,意思就是說,若是 Sub 是 Super 子類,那麼 Sub[] 是 Super[] 的子類型,而泛型是不可變,對於任意兩種類型 T1 和 T2 ,List 與 List 並不能認爲誰是誰的子類型,誰是誰的父類型,聽上去是否是感受泛型好菜啊,這都不行,不過這裏看文字理解起來比較晦澀,不如直接看代碼:

//  因爲數組是協變的,因此 Long[] 能夠賦值給 Object[] 
//  這樣是不安全的,但也是能夠編譯經過的,而後咱們在不知不覺中在 Long[] 中插入了一個 String,
//  但只有在運行時才能發現錯誤,系統會拋出 java.lang.ArrayStoreException
Object[] oArr = new Long[2];
oArr[0] = 1;
oArr[1] = "1";

//  通常來講咱們的泛型會這樣寫
List<String> strList= new ArrayList<String>();
//  Java 1.7 中,泛型加入了有限的類型推斷,因此後面的 <String> 能夠簡化爲 <>
List<String> strListNew= new ArrayList<>();
//  讓咱們回到老版本的寫法,來看看下面這樣的寫法
//  泛型是不可變的所以不支持這樣的寫法,因此這裏直接沒法經過編譯
List<ElectronicProduct> electronicProducts = new ArrayList<Phone>();
複製代碼

數組的協變特性看上去還蠻不錯的,不過實際用上去會出現類型安全問題,尤爲是當我不當心往 Long 數組中插入了一個 String 元素時,那麼代碼會在運行時拋出 ArrayStoreException 異常,由於具體類型是在運行時才知道的,而編譯時,只看到 Object[] 類型,也就致使了運行時才發現這個問題,但泛型因爲不可變特性,兩種類型並不相等,因此不能進行賦值,也就沒法經過編譯,因此書中也說,與其說是泛型有缺陷,不如說數組纔是有缺陷的,由於泛型更早的幫助咱們完成了類型安全檢查,避免了某些錯誤在運行時才暴露。

咱們真的無法讓 List electronicProducts = new ArrayList(); 它經過編譯嗎?其實稍做修改就能夠,咱們經過泛型中的有限通配符類型能夠作到,不過雖然能夠經過編譯了,可是其它的操做也被受限了,有一種我變強了也變禿了的感受,爲何會如此,那它這樣的寫法豈不是沒什麼用了?別急,一點點來看,先看爲何會約束你插入數據,由於這樣聲明的你的賦值內容是不可控的,只要是 ElectronicProduct 以及子類的 List 均可以進行賦值,就像下面代碼塊中最後四行的代碼同樣,假如它能經過編譯,那我先賦值了 Phone 類型的列表並插入數據,而後別人又操做這個列表並賦值了 PC 類型的列表給它,最後我還試圖拿出來我剛纔插入的 Phone 數據,那確定是有問題,因此爲了防止這種狀況出現,直接在插入數據時產生編譯時錯誤就能夠避免這個問題在運行時才被發現了。

//  雖然能夠經過編譯了,可是它的操做也被受限了
List<? extends ElectronicProduct> electronicProducts = new ArrayList<Phone>();
//  但你在試圖插入數據的時候會發現這時沒法經過編譯的
electronicProducts.add(new Phone());

//  哪怕改爲這樣也無法插入
List<? extends Phone> electronicProducts = new ArrayList<Phone>();
//  這裏依然沒法編譯
electronicProducts.add(new Phone());

//  好比這樣的代碼就存在風險
List<? extends ElectronicProduct> electronicProducts = new ArrayList<Phone>();
electronicProducts.add(new Phone());
//  別人修改了引用
electronicProducts = new ArrayList<PersonalComputer>();
//  我在使用使用時覺得仍是存儲的手機,這時候確定會就會出錯了
Phone phone = electronicProducts.get(0);
複製代碼

那既然 ? extends XXX 不是這麼用的,那麼它的意義是什麼呢?讓咱們來看看下面這段代碼就知道它的用途是什麼了,? extends XXX 確實放寬了對泛型類型的要求,若是某個方法的參數是 List eps,那麼就它只能接收這一種類型的泛型列表,如今它能夠接收任何 ElectronicProduct 或子類的 List,並打印出它們的電子產品信息,也就是說,這樣寫可讓使用更靈活,「雖然我不知道這個列表的具體信息,可是我知道它應該是 ElectronicProduct 或它的子類型的列表,這樣我就能夠進行產品信息打印了」,因此這麼看 ? extends XXX 仍是有它實際上的做用的,並且結合上面和下面代碼咱們能夠出它的並不適合數據插入,由於存在風險,而更適合數據返回,也就是泛型類型當作參數的方法它都不能使用,而是隻能使用泛型類型當作返回值的狀況,好比 List 中 add 的參數是泛型類型,get 的返回值是泛型類型。

public static void main(String[] args) {
        List<Phone> phones = new ArrayList<>();
        List<PersonalComputer> pcs = new ArrayList<>();
        printEPInfo(phones);
        printEPInfo(pcs);
}

//  打印電子設備相關信息
public static void printEPInfo(List<? extends ElectronicProduct> eps) {
    for (ElectronicProduct ep : eps) {
        System.out.println(ep.productName());
        System.out.println(ep.productPrice());
    }
}
複製代碼

若是理解了上面所說的,那麼 ? super XXX 就好理解好多,經歷剛纔我想把 Phone 列表賦值給 ElectronicProduct 的衝動後,那咱們能不能反過來呢?固然能夠,這時就要配合 ? super XXX 使用了,才能避免編譯時錯誤,由於它的做用也是放寬限制,只要是 Phone 自己或它的超類的列表都是賦值給它,不過這時,編譯器只知道我能夠給它賦值它自己及以上的列表類型,那麼不論我怎麼往列表中插入數據都是沒問題,大不了能夠先上轉型,但若是是取出數據就要謹慎了,就拿下面代碼中的例子來講 ElectronicProduct 列表中能夠存了它的別的子類,這時我還試圖取出數據時確定是不安全的,取出什麼類型並不能獲得保障。

//  直接寫依然沒法經過編譯
List<Phone> electronicProducts = new ArrayList<ElectronicProduct>();

//  配合 ? super XXX 就能夠經過以編譯了
List<? super Phone> electronicProducts = new ArrayList<ElectronicProduct>();
electronicProducts.add(new Phone());
System.out.println(electronicProducts.get(0));
//  這裏編譯出錯
Phone phone = electronicProducts.get(0);

//  被向上轉型存入列表
List<ElectronicProduct> electronicProducts = new ArrayList<ElectronicProduct>();
electronicProducts.add(new Phone());
electronicProducts.add(new PersonalComputer());

//  ? super XXX 舉個例子的使用場景
public <EP> void addEPToList(EP ep, List<? super EP> eps) {
        eps.add(ep);
}
複製代碼

? super XXX 是否是看着和 ? extends XXX 看着是正好相反,? super XXX 更好的使用場景是我只使用泛型類型做爲參數的方法從而放寬限制,? extends XXX 更好的使用場景是我只使用泛型類型返回值的方法從而放寬限制,前者更適合當一個輸入者,不斷往裏面放東西,後者更適合當一個輸出者,你要是吧,給你給你。固然和泛型無關的方法你們均可以調用的。

還有更寬鬆的限制符就是直接使用 ?,來看看下面的代碼,這時插入和取出都會遇到編譯時錯誤,只可使用一些和泛型類型無關的方法。

List<?> electronicProducts = new ArrayList<ElectronicProduct>();
//  不行
electronicProducts.add(new Phone());
//  仍是不行
Phone phone = electronicProducts.get(0);
electronicProducts.size();

public void test(List<?> list) {
    //  好比拿個大小
    list.size();
}
複製代碼

6.泛型方法:

說了那麼多,還沒提到泛型方法,上面的那些代碼中的方法其實都只是使用到了聲明的泛型類型而已,並無真正涉及到泛型方法,並且內容開頭的疑惑也提到了泛型信息是屬於對象的,而不是類,那咱們來看看泛型方法如何定義,其實泛型方法的泛型類型是須要單獨定義的,因此下面的例子中,NoStaticFieldGenerics 的 V 和 void print(V v) 中的 V 雖然名字同樣,可是確實不一樣的,泛型方法須要單獨聲明它的泛型類型,並且若是真的和外面的泛型類型重名的話,會覆蓋掉外面的泛型類型,就像是局部變量和對象字段重名時,局部變量優先級更高是同樣的,不過既然泛型方法的泛型類型是須要單獨聲明的,那麼它也就和對象無關了,只和具體調用時的信息有關,既然和對象無關,那麼雖然靜態泛型字段不存在,靜態泛型方法仍是能夠定義和使用的。

public class NoStaticFieldGenerics<V> {

    private V value;

    public V getValue() {
        return value;
    }

    public void setValue(V value) {
        this.value = value;
    }

    //  這裏重名的泛型類型會覆蓋掉類邊上的泛型類型
    //  因此不建議這麼寫
    public <V> void print(V v) {
        System.out.println(v);
    }

     //  這樣寫才正常
     public static  <E> void staticPrint(E v) {
        System.out.println(v);
    }
}

//  具體泛型類型爲 String
NoStaticFieldGenerics<String> n = new NoStaticFieldGenerics<>();
//  傳入 Integer,輸出 1
n.print(1);
複製代碼

寫到這裏差多把泛型的基本使用都覆蓋到了,但願我把這些內容清楚地傳達給了你。

相關文章
相關標籤/搜索