有關Java泛型,這十大知識點你必須知道

  • <strong>
  • <strong>

概述

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

泛型的本質是參數化類型,也就是所操作的數據類型被指定為一個參數。


有關Java泛型,這十大知識點你必須知道

什麼是泛型

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

泛型:把類型明確的工作推遲到創建對象或調用方法的時候才去明確的特殊的類型

參數化類型:

  • 把類型當作是參數一樣傳遞
  • 只能是引用類型

相關術語:

  • ArrayList< E >中的E稱為類型參數變量
  • ArrayList< Integer >中的Integer稱為實際類型參數
  • 整個稱為ArrayList< E >泛型類型
  • 整個ArrayList< Integer >稱為參數化的類型ParameterizedType

一個栗子

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

<code>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);}/<code>

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

<code>java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String/<code>

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

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

<code>List<string> arrayList = new ArrayList<string>();...//arrayList.add(100); 在編譯階段,編譯器就會報錯/<string>/<string>/<code>

特性

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

<code>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("泛型測試","類型相同");}/<integer>/<integer>/<string>/<string>/<code>

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

通過上面的例子可以證明,在編譯之後程序會採取去泛型化的措施。也就是說Java中的泛型,只在編譯階段有效。在編譯過程中,正確檢驗泛型結果後,會將泛型的相關信息擦出,並且在對象進入和離開方法的邊界處添加類型檢查和類型轉換的方法。也就是說,泛型信息不會進入到運行時階段。

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

有關Java泛型,這十大知識點你必須知道

泛型基礎

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

  • 泛型類
  • 泛型接口
  • 泛型方法

泛型類

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

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

<code>class 類名稱 {  private 泛型標識 /*(成員變量類型)*/ var;   .....  }}/<code>

一個最普通的泛型類:

<code>//此處T可以隨便寫為任意標識,常見的如T、E、K、V等形式的參數常用於表示泛型//在實例化泛型類時,必須指定T的具體類型public class Generic{     //key這個成員變量的類型為T,T的類型由外部指定      private T key;    public Generic(T key) { //泛型構造方法形參key的類型也為T,T的類型由外部指定        this.key = key;    }    public T getKey(){ //泛型方法getKey的返回值類型為T,T的類型由外部指定        return key;    }} 
/<code>

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

<code>    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());    }/<string>/<string>/<integer>/<integer>/<code>

結果

有關Java泛型,這十大知識點你必須知道

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

有關Java泛型,這十大知識點你必須知道

泛型接口

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

首先定義一個泛型接口

<code>package com.atguigu.ct.producer.Test;/** * 六脈神劍 * @param  */public interface Generator {    T eat();}/<code>

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

<code>package com.atguigu.ct.producer.Test;/** * 六脈神劍 * @param  */public class FruitGenerator implements Generator {    @Override    public T eat() {        return null;    }}/<code>

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

<code>package com.atguigu.ct.producer.Test;/** * 六脈神劍 * @param  */public class FruitGenerator implements Generator<string> {    @Override    public String eat() {        return null;    }}/<string>/<code>

類型通配符

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

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

<code>public void showKeyValue1(Generic<number> obj){    Log.d("泛型測試","key value is " + obj.getKey());}/<number>/<code>
<code>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);/<java.lang.number>/<java.lang.integer>/<number>/<number>/<integer>/<integer>/<code>

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

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

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

<code>public void showKeyValue1(Generic> obj){    Log.d("泛型測試","key value is " + obj.getKey());}/<code>

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

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

泛型方法

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

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

泛型類,是在實例化類的時候指明泛型的具體類型;泛型方法,是在調用方法的時候指明泛型的具體類型 。

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

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

<code>public class GenericTest {   //這個類是個泛型類,在上面已經介紹過   public class Generic{             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可以出現在這個泛型方法的任意位置.     * 泛型的數量也可以為任意多個      *    如:public  K showKeyName(Generic container){     *        ...     *        }     */    public  T showKeyName(Generic 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,並未聲明泛型類型E,因此編譯器並不知道該如何處理E這個類型。 public T showKeyName(Generic container){ ... } */ /** * 這個方法也是有問題的,編譯器會為我們提示錯誤信息:"UnKnown class 'T' " * 對於編譯器來說T這個類型並未項目中聲明過,因此編譯也不知道該如何編譯這個類。 * 所以這也不是一個正確的泛型方法聲明。 public void showkey(T genericObj){ } */ public static void main(String[] args) { }}/<number>/<number>
/<code>

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

<code>public  void printMsg( T... args){    for(T t : args){        Log.d("泛型測試","t is " + t);    }}/<code>

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

泛型上下邊界

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

為泛型添加上邊界,即傳入的類型實參必須是指定類型的子類型

<code>public void showKeyValue1(Generic extends Number> obj){    Log.d("泛型測試","key value is " + obj.getKey());/<code>
<code>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);/<double>/<double>/<float>/<float>/<integer>/<integer>/<string>/<string>/<code>

再看有個下邊界

<code>    super Type>       public TreeSet(Comparator super E> comparator) {        this(new TreeMap<>(comparator));    }/<code>

值得注意的是:無論是設定通配符上限還是下限,都是不能操作與對象有關的方法,只要涉及到了通配符,它的類型都是不確定的!

泛型的應用

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


作者:六脈神劍
鏈接:https://juejin.im/post/5df1b667f265da3398562739


分享到:


相關文章: