1. 程式人生 > >Java泛型(一)——你所不知道的那些泛型背後

Java泛型(一)——你所不知道的那些泛型背後

一、泛型

1、編譯期確定型別安全——泛型(Generics)

泛型是提供給Javac編譯器使用的。可以限定集合中輸入的型別,讓編譯器在編譯期間避免原始程式的非法輸入,編譯器編譯帶型別說明的集合時會去掉“型別”資訊,使程式執行效率不受影響,對於引數化的泛型型別,getClass()方法的返回值和原始型別完全一樣,由於編譯生成的位元組碼會去掉泛型的型別資訊,只要能跳過編譯器,就可以往某個泛型集合中加入其它型別的資料,例如,用反射得到集合,再呼叫其add方法新增自己不同的物件即可,這樣便可以實現動態內容變化化的陣列。

ArrayList類定義和ArrayList類引用中涉及如下術語:

泛型型別:

整個ArrayList<E>

型別引數(型別變數): E

引數化的型別: 整個ArrayList<Integer>

型別引數的例項: ArrayList中的Integer

typeof: ArrayList<Integer>中的<Integer>

原始型別: ArrayList

2、引數化型別與原始型別的相容性

引數化型別可以引用一個原始型別的物件,編譯報告警告,例如,   

Collection<String> c = new Vector ();

原始型別可以引用一個引數化型別的物件,編譯報告警告,例如,

Collection c = new Vector<String>();

引數化型別不考慮型別引數的繼承關係:

Vector<String> v = new Vector<Object>()    // 錯誤
Vector<Object> v = new Vector<String>()    // 也錯誤

3、型別擦除

正確理解泛型概念的首要前提是理解型別擦除(type erasure)。 Java中的泛型類似於C++中的模板,但是這種相似性僅限於表面,Java中的泛型基本上都是在編譯器這個層次來實現的。屬於編譯器執行型別檢查和型別診斷,然後生成普通的非泛型的位元組碼,也就是在生成的Java位元組程式碼中是不包含泛型中的型別資訊的,使用泛型的時候加上的型別引數,會被編譯器在編譯的時候去掉。這種實現技術稱為型別擦除。如在程式碼中定義的List<Object

>和List<String>等型別,在編譯之後都會變成List。JVM看到的只是List,而由泛型附加的型別資訊對JVM來說是不可見的。Java編譯器會在編譯時儘可能的發現可能出錯的地方,但是仍然無法避免在執行時刻出現型別轉換異常的情況。

很多泛型的奇怪特性都與這個型別擦除的存在有關,包括:

  • 泛型類並沒有自己獨有的Class類物件。比如並不存在List<String>.class或是List<Integer>.class,而只有List.class;

  • 靜態變數是被泛型類的所有例項所共享的。對於宣告為MyClass的類,訪問其中的靜態變數的方法仍然是 MyClass.myStaticVar。不管是通過new MyClass<String>還是new MyClass<Integer>建立的物件,都是共享一個靜態變數。

  • 泛型的型別引數不能用在Java異常處理的catch語句中。因為異常處理是由JVM在執行時刻來進行的。由於型別資訊被擦除,JVM是無法區分兩個異常型別MyException<String>和MyException<Integer>的。對於JVM來說,它們都是MyException型別的。也就無法執行與異常對應的catch語句。

4、泛型的定義與使用

我們使用一個泛型首先要定義它,其次就是使用它進行泛型例項化。
一般來說我們有幾種方式去確定泛型類別:

  1. 宣告時確定

  2. 執行時延後賦值確定

  3. 泛型界限確定基本型別

/*
* 泛型界限
*/
static class Apple<T extends InputStream> {
        T data;
}
/*
*正常的泛型
*/  
static class Orange<T>{
        T data;
}

    /**
     * 泛型的例項化 1:聲明確定         2:執行時確定          3:泛型界限 + 執行時確定 
     * 
     * @throws IOException
     */
public static void test3() throws IOException {

        /**
         * 1:聲明確定
         */

        Orange<InputStream> orange = new Orange<>();
        //此處編譯不通過,因為宣告時泛型引數化不會區分繼承關係,屬於精確性的宣告。
        //Orange<InputStream> orange = new Orange<FileInputStream>();

        //此處報錯,因為String並不是InputStream型別
        //orange.data = new String();

        //雖然是建立賦值,但這裡也沒有報錯。此處是由於繼承關係,FileInputStream也是InputStream,而且自動向上轉型為InputStream,但它與泛型界限並不相同,泛型界限限制了T只能是某一個類別的派生,但沒有界限的繼承則可以在宣告的時候修改T至任何一個類別,繼承自這些類別的類也都可以被向上轉型賦值,它的範圍要廣很多。
        orange.data = new FileInputStream(new File("123.txt"));
        System.out.println("1、聲明確定:"+orange.data.getClass().getName());
        System.out.println();


        /**
         * 2:執行時確定
         */
        Orange orange2 = new Orange<>();
        //此處編譯通過,因為沒有在構造的時候初始化泛型型別,因此泛型是由編譯器決定(也就是根據賦值決定)。
        orange2.data = new String();

        System.out.println("2、執行時確定:" + orange2.data.getClass().getName());
        orange2.data = new FileInputStream(new File("123.txt"));
        System.out.println("2、執行時確定:" +orange2.data.getClass().getName());
        System.out.println();

        /**
         * 3:泛型界限 + 執行時確定
         */
        Apple apple = new Apple<>();
        // 報錯,因為String不是繼承自InputStream
        // apple.data = new String();

        // 編譯通過,泛型在編譯期確定型別,因為ObjectInputStream是繼承自InputStream,因此賦值成功
        apple.data = new BufferedInputStream(new FileInputStream("123.txt"));
        System.out.println("3、泛型界限 + 執行時確定:" +apple.data.getClass().getName());
        apple.data = new FileInputStream("123.txt");
        System.out.println("3、泛型界限 + 執行時確定" +apple.data.getClass().getName());

    }

程式輸出結果:

1、聲明確定:java.io.FileInputStream

2、執行時確定:java.lang.String
2、執行時確定:java.io.FileInputStream

3、泛型界限 + 執行時確定:java.io.BufferedInputStream
3、泛型界限 + 執行時確定java.io.FileInputStream

從上面的結果我們可以看出,泛型只是我們躲過編譯器編譯的一個手段,讓我們的程式在執行時擁有更強大的靈活性,可以在編譯期間對泛型,但它也意味著我們的程式很有可能在執行時出現錯誤(轉型失敗引起程式崩潰)。

下一篇文章Java泛型(二)——使用Gson解析巢狀泛型陣列會分享一下Gson中使用泛型來解析List<E>的泛型,以及更加複雜的一種:泛型裡面還有泛型的情況,比如如何使用Gson來解析像List<CardBean<T>>這樣的資料。這種情況會存在於當我們的介面List展示的View是不同的類別時,這個時候就需要動態的去解析每一個View的Bean。