Java泛型

抽著空閑的時間學習了一下自己對Java薄弱的方面,細細的體味了一下Java泛型和反射機制,并寫了下總結,肯定會有理解不到位的地方,望勿噴指出!

Java泛型

啥叫泛型

我們直接看代碼吧:

public class Test {

    public static void main(String[] args) {
        List list = new ArrayList();
        list.add("tjun");
        list.add("aswddads");
        list.add(99);

        for (int i = 0; i < list.size(); i++) {
            String name = (String) list.get(i); // 1
            System.out.println("name:" + name);
        }
    }
}

這段代碼編譯是會通過,但是在程序運行就會崩潰

java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

為啥尼

我們知道對于Arraylist,它可以存入任何類型,代碼中我們先add進了兩個String類型,再add進了一個Integer類型,這是完全允許的;接下來,我們遍歷取出list中的元素,這時候程序就崩了;異常信息如崩潰日志一致,而且此類錯誤可能稍不注意就會犯,因為在編譯階段能夠通過;其實主要問題出現在以下兩點:

1.將對象放入集合中,對于集合是不會記住此對象的類型,當再次從集合中取出對象時,該對象的編譯類型變成了Object類型,但其運行時類型依然為其本身類型;
2.在代碼//1處取出集合元素時需要人為的強制類型轉化到具體的目標類型,且很容易出現“java.lang.ClassCastException”異常。

對于使集合記住元素類型,且編譯時不出問題,運行時也不出現”java.lang.ClassCastException“,解決的辦法就是利用泛型。
好吧,都說到這了,我們就進入概念時間吧:

泛型

泛型,即“參數化類型”,可能提到參數都會想到我們在寫Java方法的時候都會有形參,實參;對于參數化類型我的理解就是,將類型由原來的具體類型參數化,類似于方法中的變量參數,可稱之為類型形參,然后在使用的時候傳入具體的類型。
看著這理論性的概念很頭痛吧,來看看對于上個例子中的代碼進行改進,你就會理解了:

public class Test {

    public static void main(String[] args) {
        /*
        List list = new ArrayList();
        list.add("qqyumidi");
        list.add("corn");
        list.add(100);
        */

        List<String> list = new ArrayList<String>();
        list.add("qqyumidi");
        list.add("corn");
        //list.add(100);   // 1  提示編譯錯誤

        for (int i = 0; i < list.size(); i++) {
            String name = list.get(i); // 2
            System.out.println("name:" + name);
        }
    }
}

我們利用泛型的寫法后,我們在編寫代碼的時候,//1處就會報出編譯錯誤;因為我們在開始的時候就直接通過List<String>限定了在list集合中只能夠含有String類型的元素,從而在//2處無須進行強制類型轉換,因為此時,集合能夠記住元素的類型信息,編譯器已經能夠確認它是String類型了。

泛型的特性

一句話,泛型只在編譯階段有效,上代碼大餐:

public class Test {

    public static void main(String[] args) {
       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("泛型測試","類型相同");
       }
    }
}

你們會發現輸出結果是

泛型測試:類型相同

因此,結論就是:泛型只在編譯階段有效。

在編譯之后程序會采取去泛型化的措施。也就是說Java中的泛型,只在編譯階段有效。在編譯過程中,正確檢驗泛型結果后,會將泛型的相關信息擦出,并且在對象進入和離開方法的邊界處添加類型檢查和類型轉換的方法。也就是說,成功編譯過后的class文件中是不包含任何泛型信息的,泛型信息不會進入到運行時階段。

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

泛型使用

對于泛型的使用無非就是泛型類、泛型方法、泛型接口;
我們查看List、ArrayList源碼,就可以看到泛型的三種使用.

但是下面我給出一個更簡單的例子:

class Book<T> {

    private T data;

    public Book() {

    }

    public Book(T data) {
        this.data = data;
    }

    public T getData() {
        return data;
    }

}

我們常見的如T、E、K、V等形式的參數常用于表示泛型形參,由于接收來自外部使用時候傳入的類型實參。

在使用泛型的時候如果傳入泛型實參,則會根據傳入的泛型實參做相應的限制,此時泛型才會起到本應起到的限制作用。如果不傳入泛型類型實參的話,在泛型類中使用泛型的方法或成員變量定義的類型可以為任何的類型。
給你個例子看看:

Book b1 = new Book("111111");
Book b2 = new Book(4444);
Book b3 = new Book(55.55);
Book b4 = new Book(false);

Log.d("測試","key is " + b1.getData()); //測試key is 111111
Log.d("測試","key is " + b2.getData()); //測試key is 4444
Log.d("測試","key is " + b3.getData()); //測試key is 55.55
Log.d("測試","key is " + b4.getData()); //測試key is false

總結: 1.泛型的類型參數只能是類類型,不能是簡單類型
2.不能對確切的泛型類型使用instanceof操作。如下面的操作是非法的,編譯時會出錯。

if(b4 instanceof Book<StoryBook>){
}

接口

接口常用在類產生器中:

//定義一個泛型接口
public interface Book<T> {
    public T next();
}

當實現泛型接口的類,未傳入泛型實參時:

/**
 * 未傳入泛型實參時,與泛型類的定義相同,在聲明類的時候,需將泛型的聲明也一起加到類中
 * 即:class  StoryBook<T> implements Book<T>{
 * 如果不聲明泛型,如:class StoryBook implements Book<T>,編譯器會報錯:"Unknown class"
 */
class StoryBook<T> implements Book<T>{
    @Override
    public T next() {
        return null;
    }
}

當實現泛型接口的類,傳入泛型實參時:

/**
 * 傳入泛型實參時:
 * 定義一個生產器實現這個接口,雖然我們只創建了一個泛型接口Book<T>
 * 但是我們可以為T傳入無數個實參,形成無數種類型的Book接口。
 * 在實現類實現泛型接口時,如已將泛型類型傳入實參類型,則所有使用泛型的地方都要替換成傳入的實參類型
 * 即:Book<T>,public T next();中的的T都要替換成傳入的String類型。
 */
public class StoryBook implements Book<String> {

    private String[] books = new String[]{"b1", "b2", "b3"};

    @Override
    public String next() {
        Random rand = new Random();
        return books[rand.nextInt(3)];
    }
}

泛型方法

泛型類與泛型方法的區別在于:泛型類是在實例化類的時候指明泛型的具體類型,而泛型方法是在調用方法的時候指明泛型的具體類型,通過一段代碼介紹一下泛型吧。

/**
 * 泛型方法的基本介紹
 * @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;
}

或許這樣可能還是還是很迷惑,可以給看看我看的一個大神博客的代碼演示一下:

public class BookTest {
   public class Book<T>{     
        private T key;

        public Book(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> T showKeyName(Book<T> container){
     *        ...
     *        }
     */
    public <T> T showKeyName(Book<T> container){
        System.out.println("container key :" + container.getKey());
        //當然這個例子舉的不太合適,只是為了說明泛型方法的特性。
        T test = container.getKey();
        return test;
    }

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

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

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

    /**
     * 這個方法也是有問題的,編譯器會為我們提示錯誤信息:"UnKnown class 'T' "
     * 對于編譯器來說T這個類型并未項目中聲明過,因此編譯也不知道該如何編譯這個類。
     * 所以這也不是一個正確的泛型方法聲明。
    public void showkey(T genericObj){

    }
    */

    public static void main(String[] args) {


    }
}

通過上面這些例子,對于泛型方法的使用應該更得心應手了吧。
剛剛在里面看到有泛型通配符,那么在這里插講講泛型通配符。

泛型通配符

通過泛型的特性我們知道Book<Number>和Book<Integer>都是Book類型,那么請我們思考一下是否可以Book<Number>和Book<Integer>是存在繼承關系的泛型?
我們來看一段代碼:

public class BookTest {

    public static void main(String[] args) {

        Book<Number> name = new Book<Number>(99);
        Book<Integer> age = new Book<Integer>(712);

        getData(name);
 
        getData(age);  //1
        //The method getData(Book<Number>) in the type BookTest is 
        //not applicable for the arguments (Book<Integer>)

    }
    
    public static void getData(Book<Number> data){
        System.out.println("data :" + data.getData());
    }

}

代碼在//1處出現了錯誤提示信息,表示Book<Number>和Book<Integer>在邏輯上不能視為繼承關系。
我們再來看一段代碼:

public class BookTest {

    public static void main(String[] args) {

        Book<Integer> b1 = new Book<Integer>(712);
        Book<Number> b2 = b1;  // 1
        Book<Float> f = new Book<Float>(3.14f);
        b2.setData(f);        // 2

    }

    public static void getData(Book<Number> data) {
        System.out.println("data :" + data.getData());
    }

}

class Book<T> {

    private T data;

    public Book() {

    }

    public Book(T data) {
        setData(data);
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

}

這段代碼,我們一眼就知道在//1和//2處肯定會提示錯誤;運用數學中的證明方法,我們不妨設Book<Number>在邏輯上可以視為Book<Integer>的父類,那么//1和//2處將不會有錯誤提示了;
那么問題就出來了,通過getData()方法取出數據時到底是什么類型呢?Integer? Float? 還是Number?
在編程過程中的順序不可控性,導致在必要的時候必須要進行類型判斷,且進行強制類型轉換。顯然,這與泛型的理念矛盾,因此,在邏輯上Book<Number>不能視為Book<Integer>的父類。

通過實踐,我們知道其具體的錯誤提示的深層次原因了。那么如何解決呢?總不能再定義一個新的函數吧。這和Java中的多態理念顯然是違背的,因此,我們需要一個在邏輯上可以用來表示同時是Book<Integer>和Book<Number>的父類的一個引用類型,由此,類型通配符(?)就該是我們考慮的了。

類型通配符使用?代替具體的類型實參,那么Book<?>就可以在邏輯上成為Book<Number>和Book<Integer>的父類,因此對上面代碼進行改進:

public class BookTest {

    public static void main(String[] args) {

        Book<String> string = new Book<String>("tjun");
        Book<Integer> integer = new Book<Integer>(99);
        Book<Number> number = new Book<Number>(100);

        getData(string);
        getData(integer);
        getData(number);
    }

    public static void getData(Book<?> data) {
        System.out.println("data :" + data.getData());
    }

}

使用類型通配符就解決了上面的問題。

通配符上下限

談到類型通配符,那么就一定要說說類型通配符上限和類型通配符下限;
比如我們對上面的例子有了新需求,定義一個功能類似于getData()的方法,但對類型實參又有進一步的限制:只能是Number類及其子類。此時,需要用到類型通配符上限(Book<? extends Number> data)。看看代碼:

public class BookTest {

    public static void main(String[] args) {

        Book<String> string = new Book<String>("tjun");
        Book<Integer> integer = new Book<Integer>(99);
        Book<Number> number = new Book<Number>(100);

        getData(string);
        getData(integer);
        getData(number);
        
        //getUpperNumberData(string); // 1  錯誤提示
        getUpperNumberData(integer);    // 2 正常
        getUpperNumberData(number); // 3 正常
    }

    public static void getData(Book<?> data) {
        System.out.println("data :" + data.getData());
    }
    
    public static void getUpperNumberData(Book<? extends Number> data){
        System.out.println("data :" + data.getData());
    }

}

類型通配符上限通過形如Book<? extends Number>形式定義,相對應的,類型通配符下限為Book<? super Number>形式,其含義與類型通配符上限正好相反.

我們繼續回到泛型的方法來說幾句。

類中的泛型方法

泛型方法可以出現雜任何地方和任何場景中使用。但是有一種情況是非常特殊的,當泛型方法出現在泛型類中時,我們再通過一個例子看一下:

public class Test {
    class Book{
        @Override
        public String toString() {
            return "book";
        }
    }

    class StoryBook extends Book{
        @Override
        public String toString() {
            return "storyBook";
        }
    }

    class Love{
        @Override
        public String toString() {
            return "love";
        }
    }

    class BookTest<T>{
        public void show_1(T t){
            System.out.println(t.toString());
        }

        //在泛型類中聲明了一個泛型方法,使用泛型E,這種泛型E可以為任意類型。可以類型與T相同,也可以不同。
        //由于泛型方法在聲明的時候會聲明泛型<E>,因此即使在泛型類中并未聲明泛型,編譯器也能夠正確識別泛型方法中識別的泛型。
        public <E> void show_3(E t){
            System.out.println(t.toString());
        }

        //在泛型類中聲明了一個泛型方法,使用泛型T,注意這個T是一種全新的類型,可以與泛型類中聲明的T不是同一種類型。
        public <T> void show_2(T t){
            System.out.println(t.toString());
        }
    }

    public static void main(String[] args) {
       StoryBook storyBook = new StoryBook();
       Love love = newLove();

        BookTest<Book> bookTest = new BookTest<Book>();
        //StoryBook是Book的子類,所以這里可以
        bookTest.show_1(storyBook);
        //編譯器會報錯,因為泛型類型實參指定的是Book,而傳入的實參類是Love
        //bookTest.show_1(love);

        //使用這兩個方法都可以成功
        bookTest.show_2(storyBook);
        bookTest.show_2(love);

        //使用這兩個方法也都可以成功
        BookTest.show_3(storyBook);
        BookTest.show_3(love);
    }
}

泛型方法還有一個重要的知識點就是可變參數得提一下:

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

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

泛型與靜態方法

方法要使用泛型的話,必須將靜態方法也定義成泛型方法 。

public class StaticTest<T> {
    ....
    /**
     * 如果在類中定義使用泛型的靜態方法,需要添加額外的泛型聲明(將這個方法定義成泛型方法)
     * 即使靜態方法要使用泛型類中已經聲明過的泛型也不可以。
     * 如:public static void show(T t){..},此時編譯器會提示錯誤信息:
          "StaticTest cannot be refrenced from static context"
     */
    public static <T> void show(T t){

    }
}

即:如果靜態方法要使用泛型的話,必須將靜態方法也定義成泛型方法 ;靜態方法無法訪問類上定義的泛型;如果靜態方法操作的引用數據類型不確定的時候,必須要將泛型定義在方法上。

泛型總結

泛型方法能使方法獨立于類而產生變化,以下是一個基本的指導原則:

無論何時,如果你能做到,你就該盡量使用泛型方法。也就是說,如果使用泛型方法將整個類泛型化,那么就應該使用泛型方法。另外對于一個static的方法而已,無法訪問泛型類型的參數。所以如果static方法要使用泛型能力,就必須使其成為泛型方法。

先就總結下泛型的學習吧,對于反射就接下來有時間就補充上。

最后,鏈上一個博客介紹泛型不錯的文章。

第一次寫博客,望指正一起學習,輕噴,謝謝!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 開發人員在使用泛型的時候,很容易根據自己的直覺而犯一些錯誤。比如一個方法如果接收List作為形式參數,那么如果嘗試...
    時待吾閱讀 1,072評論 0 3
  • 前面,由于對泛型擦除的思考,引出了對Java-Type體系的學習。本篇,就讓我們繼續對“泛型”進行研究: JDK1...
    賈博巖閱讀 5,194評論 3 28
  • object 變量可指向任何類的實例,這讓你能夠創建可對任何數據類型進程處理的類。然而,這種方法存在幾個嚴重的問題...
    CarlDonitz閱讀 934評論 0 5
  • 去年年底,公司搬到新職場,辦公室為每個部門發了綠蘿,不過是為了裝飾光禿禿的辦公環境。 我們屬于業務部門,平日里職場...
    梓涵919閱讀 833評論 0 3
  • 音樂《時間煮雨》 小A是一個大大咧咧又有點女漢子風范的女孩,但她卻有一顆狂熱的心,對生活、對愛情。所以,聽到她與男...
    傻傻女孩閱讀 440評論 3 3