?你必須知道的Java泛型

六脈神劍發表於2019-12-24

前言

文字已收錄至我的GitHub倉庫,歡迎Star:github.com/bin39232820…
種一棵樹最好的時間是十年前,其次是現在
我知道很多人不玩qq了,但是懷舊一下,歡迎加入六脈神劍Java菜鳥學習群,群聊號碼:549684836 鼓勵大家在技術的路上寫部落格

絮叨

其實寫的每一篇文章都是有原因的,我本來在寫Java 8的新特性,但是學著,學著發現裡面很多函式式介面,對泛型的要求挺高,所以決定把泛型的知識搞定先,反正不管怎麼樣,這個坑還是我們必須要去踩的,上面一篇內部類的文章:
?你不知道的Java內部類

概述

Java泛型(generics)是JDK 5中引入的一個新特性,泛型提供了編譯時型別安全監測機制,該機制允許程式設計師在編譯時監測非法的型別。使用泛型機制編寫的程式程式碼要比那些雜亂地使用Object變數,然後再進行強制型別轉換的程式碼具有更好的安全性和可讀性。泛型對於集合類尤其有用,例如,ArrayList就是一個無處不在的集合類。

泛型的本質是引數化型別,也就是所操作的資料型別被指定為一個引數。

什麼是泛型

Java泛型設計原則:只要在編譯時期沒有出現警告,那麼執行時期就不會出現ClassCastException異常

泛型:把型別明確的工作推遲到建立物件或呼叫方法的時候才去明確的特殊的型別

引數化型別:

  • 把型別當作是引數一樣傳遞

  • <資料型別> 只能是引用型別

相關術語:

  • ArrayList< E >中的E稱為型別引數變數

  • ArrayList< Integer >中的Integer稱為實際型別引數

  • 整個稱為ArrayList< E >泛型型別

  • 整個ArrayList< Integer >稱為引數化的型別ParameterizedType

一個栗子

一個被舉了無數次的例子:

List arrayList = new ArrayList();
arrayList.add("六脈神劍");
arrayList.add(100);

for(int i = 0; i< arrayList.size();i++){
    String item = (String)arrayList.get(i);
    Log.d("泛型測試","item = " + item);
}
複製程式碼

毫無疑問,程式的執行結果會以崩潰結束:

java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
複製程式碼

ArrayList可以存放任意型別,例子中新增了一個String型別,新增了一個Integer型別,再使用時都以String的方式使用,因此程式崩潰了。為了解決類似這樣的問題(在編譯階段就可以解決),泛型應運而生。

我們將第一行宣告初始化list的程式碼更改一下,編譯器會在編譯階段就能夠幫我們發現類似這樣的問題。

List<String> arrayList = new ArrayList<String>();
...
//arrayList.add(100); 在編譯階段,編譯器就會報錯
複製程式碼

特性

泛型只在編譯階段有效。看下面的程式碼:

List<String> stringArrayList = new ArrayList<String>();
List<Integer> integerArrayList = new ArrayList<Integer>();

Class classStringArrayList = stringArrayList.getClass();
Class classIntegerArrayList = integerArrayList.getClass();

if(classStringArrayList.equals(classIntegerArrayList)){
    Log.d("泛型測試","型別相同");
}

複製程式碼

輸出結果:D/泛型測試: 型別相同。

通過上面的例子可以證明,在編譯之後程式會採取去泛型化的措施。也就是說Java中的泛型,只在編譯階段有效。在編譯過程中,正確檢驗泛型結果後,會將泛型的相關資訊擦出,並且在物件進入和離開方法的邊界處新增型別檢查和型別轉換的方法。也就是說,泛型資訊不會進入到執行時階段。

對此總結成一句話:泛型型別在邏輯上看以看成是多個不同的型別,實際上都是相同的基本型別。

泛型基礎

泛型有三種使用方式,分別為:

  • 泛型類
  • 泛型介面
  • 泛型方法

泛型類

泛型型別用於類的定義中,被稱為泛型類。通過泛型可以完成對一組類的操作對外開放相同的介面。最典型的就是各種容器類,如:List、Set、Map。

泛型類的最基本寫法(這麼看可能會有點暈,會在下面的例子中詳解):

class 類名稱 <泛型標識:可以隨便寫任意標識號,標識指定的泛型的型別>{
  private 泛型標識 /*(成員變數型別)*/ var; 
  .....

  }
}
複製程式碼

一個最普通的泛型類:

//此處T可以隨便寫為任意標識,常見的如T、E、K、V等形式的引數常用於表示泛型
//在例項化泛型類時,必須指定T的具體型別
public class Generic<T>{ 
    //key這個成員變數的型別為T,T的型別由外部指定  
    private T key;

    public Generic(T key) { //泛型構造方法形參key的型別也為T,T的型別由外部指定
        this.key = key;
    }

    public T getKey(){ //泛型方法getKey的返回值型別為T,T的型別由外部指定
        return key;
    }
}
複製程式碼

使用者想要使用哪種型別,就在建立的時候指定型別。使用的時候,該類就會自動轉換成使用者想要使用的型別了。

    public static void main(String[] args) {
        //泛型的型別引數只能是類型別(包括自定義類),不能是簡單型別
//傳入的實參型別需與泛型的型別引數型別相同,即為Integer.
        Generic<Integer> genericInteger = new Generic<Integer>(123456);

//傳入的實參型別需與泛型的型別引數型別相同,即為String.
        Generic<String> genericString = new Generic<String>("key_vlaue");
        System.out.println( genericString.getKey());
    }
複製程式碼

結果

?你必須知道的Java泛型
定義的泛型類,就一定要傳入泛型型別實參麼?並不是這樣,在使用泛型的時候如果傳入泛型實參,則會根據傳入的泛型實參做相應的限制,此時泛型才會起到本應起到的限制作用。如果不傳入泛型型別實參的話,在泛型類中使用泛型的方法或成員變數定義的型別可以為任何的型別。

?你必須知道的Java泛型

泛型介面

泛型介面與泛型類的定義及使用基本相同。泛型介面常被用在各種類的生產器中,可以看一個例子:

首先定義一個泛型介面

package com.atguigu.ct.producer.Test;

/**
 * 六脈神劍
 * @param <T>
 */
public interface Generator<T> {
    T eat();
}

複製程式碼

當實現泛型介面的類,未傳入泛型實參時: 由於沒有傳入具體的引數,所以這個實現介面的類,也必然是泛型類,不然編譯不過

package com.atguigu.ct.producer.Test;

/**
 * 六脈神劍
 * @param <T>
 */
public class FruitGenerator<T> implements Generator<T> {
    @Override
    public T eat() {
        return null;
    }
}

複製程式碼

當實現泛型介面的類,傳入泛型實參時:這個時候就不能是泛型類了,因為泛型介面已經明確型別了

package com.atguigu.ct.producer.Test;

/**
 * 六脈神劍
 * @param 
 */
public class FruitGenerator implements Generator<String> {
    @Override
    public String eat() {
        return null;
    }
}

複製程式碼

型別萬用字元

我們知道Ingeter是Number的一個子類,同時在特性章節中我們也驗證過Generic與Generic實際上是相同的一種基本型別。那麼問題來了,在使用Generic作為形參的方法中,能否使用Generic的例項傳入呢?在邏輯上類似於Generic和Generic是否可以看成具有父子關係的泛型型別呢?

為了弄清楚這個問題,我們使用Generic這個泛型類繼續看下面的例子:

public void showKeyValue1(Generic<Number> obj){
    Log.d("泛型測試","key value is " + obj.getKey());
}
複製程式碼
Generic<Integer> gInteger = new Generic<Integer>(123);
Generic<Number> gNumber = new Generic<Number>(456);

showKeyValue(gNumber);

// showKeyValue這個方法編譯器會為我們報錯:Generic<java.lang.Integer> 
// cannot be applied to Generic<java.lang.Number>
// showKeyValue(gInteger);
複製程式碼

通過提示資訊我們可以看到Generic不能被看作為`Generic的子類。由此可以看出:同一種泛型可以對應多個版本(因為引數型別是不確定的),不同版本的泛型類例項是不相容的。

回到上面的例子,如何解決上面的問題?總不能為了定義一個新的方法來處理Generic型別的類,這顯然與java中的多臺理念相違背。因此我們需要一個在邏輯上可以表示同時是Generic和Generic父類的引用型別。由此型別萬用字元應運而生。

我們可以將上面的方法改一下:

public void showKeyValue1(Generic<?> obj){
    Log.d("泛型測試","key value is " + obj.getKey());
}
複製程式碼

型別萬用字元一般是使用?代替具體的型別實參,注意了,此處’?’是型別實參,而不是型別形參 。重要說三遍!此處’?’是型別實參,而不是型別形參 ! 此處’?’是型別實參,而不是型別形參 !再直白點的意思就是,此處的?和Number、String、Integer一樣都是一種實際的型別,可以把?看成所有型別的父類。是一種真實的型別。

可以解決當具體型別不確定的時候,這個萬用字元就是 ? ;當操作型別時,不需要使用型別的具體功能時,只使用Object類中的功能。那麼可以用 ? 萬用字元來表未知型別。

泛型方法

在java中,泛型類的定義非常簡單,但是泛型方法就比較複雜了。

尤其是我們見到的大多數泛型類中的成員方法也都使用了泛型,有的甚至泛型類中也包含著泛型方法,這樣在初學者中非常容易將泛型方法理解錯了。

泛型類,是在例項化類的時候指明泛型的具體型別;泛型方法,是在呼叫方法的時候指明泛型的具體型別 。

/**
 * 泛型方法的基本介紹
 * @param tClass 傳入的泛型實參
 * @return T 返回值為T型別
 * 說明:
 *     1)public 與 返回值中間<T>非常重要,可以理解為宣告此方法為泛型方法。
 *     2)只有宣告瞭<T>的方法才是泛型方法,泛型類中的使用了泛型的成員方法並不是泛型方法。
 *     3)<T>表明該方法將使用泛型型別T,此時才可以在方法中使用泛型型別T。
 *     4)與泛型類的定義一樣,此處T可以隨便寫為任意標識,常見的如T、E、K、V等形式的引數常用於表示泛型。
 */
public <T> T genericMethod(Class<T> tClass)throws InstantiationException ,
  IllegalAccessException{
        T instance = tClass.newInstance();
        return instance;
}
Object obj = genericMethod(Class.forName("com.test.test"));

複製程式碼

光看上面的例子有的同學可能依然會非常迷糊,我們再通過一個例子,把我泛型方法再總結一下。

public class GenericTest {
   //這個類是個泛型類,在上面已經介紹過
   public class Generic<T>{     
        private T key;

        public Generic(T key) {
            this.key = key;
        }

        //我想說的其實是這個,雖然在方法中使用了泛型,但是這並不是一個泛型方法。
        //這只是類中一個普通的成員方法,只不過他的返回值是在宣告泛型類已經宣告過的泛型。
        //所以在這個方法中才可以繼續使用 T 這個泛型。
        public T getKey(){
            return key;
        }

        /**
         * 這個方法顯然是有問題的,在編譯器會給我們提示這樣的錯誤資訊"cannot reslove symbol E"
         * 因為在類的宣告中並未宣告泛型E,所以在使用E做形參和返回值型別時,編譯器會無法識別。
        public E setKey(E key){
             this.key = keu
        }
        */
    }

    /** 
     * 這才是一個真正的泛型方法。
     * 首先在public與返回值之間的<T>必不可少,這表明這是一個泛型方法,並且宣告瞭一個泛型T
     * 這個T可以出現在這個泛型方法的任意位置.
     * 泛型的數量也可以為任意多個 
     *    如:public <T,K> K showKeyName(Generic<T> container){
     *        ...
     *        }
     */
    public <T> T showKeyName(Generic<T> container){
        System.out.println("container key :" + container.getKey());
        //當然這個例子舉的不太合適,只是為了說明泛型方法的特性。
        T test = container.getKey();
        return test;
    }

    //這也不是一個泛型方法,這就是一個普通的方法,只是使用了Generic<Number>這個泛型類做形參而已。
    public void showKeyValue1(Generic<Number> obj){
        Log.d("泛型測試","key value is " + obj.getKey());
    }

    //這也不是一個泛型方法,這也是一個普通的方法,只不過使用了泛型萬用字元?
    //同時這也印證了泛型萬用字元章節所描述的,?是一種型別實參,可以看做為Number等所有類的父類
    public void showKeyValue2(Generic<?> obj){
        Log.d("泛型測試","key value is " + obj.getKey());
    }

     /**
     * 這個方法是有問題的,編譯器會為我們提示錯誤資訊:"UnKnown class 'E' "
     * 雖然我們宣告瞭<T>,也表明了這是一個可以處理泛型的型別的泛型方法。
     * 但是隻宣告瞭泛型型別T,並未宣告泛型型別E,因此編譯器並不知道該如何處理E這個型別。
    public <T> T showKeyName(Generic<E> container){
        ...
    }  
    */

    /**
     * 這個方法也是有問題的,編譯器會為我們提示錯誤資訊:"UnKnown class 'T' "
     * 對於編譯器來說T這個型別並未專案中宣告過,因此編譯也不知道該如何編譯這個類。
     * 所以這也不是一個正確的泛型方法宣告。
    public void showkey(T genericObj){

    }
    */

    public static void main(String[] args) {


    }
}
複製程式碼

再看一個泛型方法和可變引數的例子:

public <T> void printMsg( T... args){
    for(T t : args){
        Log.d("泛型測試","t is " + t);
    }
}
複製程式碼

printMsg("111",222,"aaaa","2323.4",55.55);

泛型上下邊界

在使用泛型的時候,我們還可以為傳入的泛型型別實參進行上下邊界的限制,如:型別實參只准傳入某種型別的父類或某種型別的子類。

為泛型新增上邊界,即傳入的型別實參必須是指定型別的子型別

public void showKeyValue1(Generic<? extends Number> obj){
    Log.d("泛型測試","key value is " + obj.getKey());
複製程式碼
Generic<String> generic1 = new Generic<String>("11111");
Generic<Integer> generic2 = new Generic<Integer>(2222);
Generic<Float> generic3 = new Generic<Float>(2.4f);
Generic<Double> generic4 = new Generic<Double>(2.56);

//這一行程式碼編譯器會提示錯誤,因為String型別並不是Number型別的子類
//showKeyValue1(generic1);

showKeyValue1(generic2);
showKeyValue1(generic3);
showKeyValue1(generic4);
複製程式碼

再看有個下邊界

   <? super Type>
       public TreeSet(Comparator<? super E> comparator) {
        this(new TreeMap<>(comparator));
    }
複製程式碼

值得注意的是:無論是設定萬用字元上限還是下限,都是不能操作與物件有關的方法,只要涉及到了萬用字元,它的型別都是不確定的!

泛型的應用

最簡單 我們的Dao 我的service impl 肯定是用到了的 這樣可以封裝一些通用的方法了

結尾

泛型就講那麼多了,希望對大家有所幫助,至少對泛型的認識深刻了點,但是要熟練運用,我只能說冰凍三尺,非一日之寒,這些才是真正的手上功夫,各位大佬一起加油。

日常求贊

好了各位,以上就是這篇文章的全部內容了,能看到這裡的人呀,都是人才

創作不易,各位的支援和認可,就是我創作的最大動力,我們下篇文章見

六脈神劍 | 文 【原創】如果本篇部落格有任何錯誤,請批評指教,不勝感激 !

相關文章