Java 泛型總結(二):泛型與數組

簡介

上一篇文章介紹了泛型的基本用法以及類型擦除的問題,如今來看看泛型和數組的關係。數組相比於Java 類庫中的容器類是比較特殊的,主要體如今三個方面:java

  1. 數組建立後大小便固定,但效率更高
  2. 數組能追蹤它內部保存的元素的具體類型,插入的元素類型會在編譯期獲得檢查
  3. 數組能夠持有原始類型 ( int,float等 ),不過有了自動裝箱,容器類看上去也能持有原始類型了

那麼當數組遇到泛型會怎樣? 可否建立泛型數組呢?這是這篇文章的主要內容。編程

這個系列的另外兩篇文章:segmentfault

泛型數組

如何建立泛型數組

若是有一個類以下:數組

class Generic<T> {
    
}

若是要建立一個泛型數組,應該是這樣: Generic<Integer> ga = new Generic<Integer>[]。不過行代碼會報錯,也就是說不能直接建立泛型數組。設計

那麼若是要使用泛型數組怎麼辦?一種方案是使用 ArrayList,好比下面的例子:code

public class ListOfGenerics<T> {
    private List<T> array = new ArrayList<T>();
    public void add(T item) { array.add(item); }
    public T get(int index) { return array.get(index); }
}

如何建立真正的泛型數組呢?咱們不能直接建立,但能夠定義泛型數組的引用。好比:對象

public class ArrayOfGenericReference {
    static Generic<Integer>[] gia;
}

gia 是一個指向泛型數組的引用,這段代碼能夠經過編譯。可是,咱們並不能建立這個確切類型的數組,也就是不能使用 new Generic<Integer>[]。具體參見下面的例子:get

public class ArrayOfGeneric {
    static final int SIZE = 100;
    static Generic<Integer>[] gia;
    @SuppressWarnings("unchecked")
    public static void main(String[] args) {
        // Compiles; produces ClassCastException:
        //! gia = (Generic<Integer>[])new Object[SIZE];
        // Runtime type is the raw (erased) type:
        gia = (Generic<Integer>[])new Generic[SIZE];
        System.out.println(gia.getClass().getSimpleName());
        gia[0] = new Generic<Integer>();
        //! gia[1] = new Object(); // Compile-time error
        // Discovers type mismatch at compile time:
        //! gia[2] = new Generic<Double>();
        Generic<Integer> g = gia[0];
    }
} /*輸出:
Generic[]
*///:~

數組能追蹤元素的實際類型,這個類型是在數組建立的時候創建的。上面被註釋掉的一行代碼: gia = (Generic<Integer>[])new Object[SIZE],數組在建立的時候是一個 Object 數組,若是轉型便會報錯。成功建立泛型數組的惟一方式是建立一個類型擦除的數組,而後轉型,如代碼: gia = (Generic<Integer>[])new Generic[SIZE]giaClass 對象輸出的名字是 Generic[]編譯器

我我的的理解是:因爲類型擦除,因此 Generic<Integer> 至關於初始類型 Generic,那麼 gia = (Generic<Integer>[])new Generic[SIZE] 中的轉型其實仍是轉型爲 Generic[],看上去像沒轉,可是多了編譯器對參數的檢查和自動轉型,向數組插入 new Object()new Generic<Double>() 均會報錯,而 gia[0] 取出給 Generic<Integer> 也不須要咱們手動轉型。it

使用 T[] array

上面的例子中,元素的類型是泛型類。下面看一個元素自己類型是泛型參數的例子:

public class GenericArray<T> {
    private T[] array;
    @SuppressWarnings("unchecked")
    public GenericArray(int sz) {
        array = (T[])new Object[sz];   // 建立泛型數組
    }
    public void put(int index, T item) {
        array[index] = item;
    }
    public T get(int index) { return array[index]; }
    // Method that exposes the underlying representation:
    public T[] rep() { return array; }     //返回數組 會報錯
    public static void main(String[] args) {
        GenericArray<Integer> gai =
        new GenericArray<Integer>(10);
        // This causes a ClassCastException:
        //! Integer[] ia = gai.rep();
        // This is OK:
        Object[] oa = gai.rep();
    }
}

在上面的代碼中,泛型數組的建立是建立一個 Object 數組,而後轉型爲 T[]。但數組實際的類型仍是 Object[]。在調用 rep()方法的時候,就報 ClassCastException 異常了,由於 Object[] 沒法轉型爲 Integer[]

那建立泛型數組的代碼 array = (T[])new Object[sz] 爲何不會報錯呢?個人理解和前面介紹的相似,因爲類型擦除,至關於轉型爲 Object[],看上去就是沒轉,可是多了編譯器的參數檢查和自動轉型。而若是把泛型參數改爲 <T extends Integer>,那麼由於類型是擦除到第一個邊界,因此 array = (T[])new Object[sz] 中至關於轉型爲 Integer[],這應該會報錯。下面是實驗的代碼:

public class GenericArray<T extends Integer> {
    private T[] array;
    @SuppressWarnings("unchecked")
    public GenericArray(int sz) {
        array = (T[])new Object[sz];   // 建立泛型數組
    }
    public void put(int index, T item) {
        array[index] = item;
    }
    public T get(int index) { return array[index]; }
    // Method that exposes the underlying representation:
    public T[] rep() { return array; }     //返回數組 會報錯
    public static void main(String[] args) {
        GenericArray<Integer> gai =
        new GenericArray<Integer>(10);
        // This causes a ClassCastException:
        //! Integer[] ia = gai.rep();
        // This is OK:
        Object[] oa = gai.rep();
    }
}

相比於原始的版本,上面的代碼只修改了第一行,把 <T> 改爲了 <T extends Integer>,那麼不用調用 rep(),在建立泛型數組的時候就會報錯。下面是運行結果:

Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer;
at GenericArray.<init>(GenericArray.java:15)

使用 Object[] array

因爲擦除,運行期的數組類型只能是 Object[],若是咱們當即把它轉型爲 T[],那麼在編譯期就失去了數組的實際類型,編譯器也許沒法發現潛在的錯誤。所以,更好的辦法是在內部最好使用 Object[] 數組,在取出元素的時候再轉型。看下面的例子:

public class GenericArray2<T> {
    private Object[] array;
    public GenericArray2(int sz) {
        array = new Object[sz];
    }
    public void put(int index, T item) {
        array[index] = item;
    }
    @SuppressWarnings("unchecked")
    public T get(int index) { return (T)array[index]; }
    @SuppressWarnings("unchecked")
    public T[] rep() {
        return (T[])array; // Warning: unchecked cast
    }
    public static void main(String[] args) {
        GenericArray2<Integer> gai =
        new GenericArray2<Integer>(10);
        for(int i = 0; i < 10; i ++)
        gai.put(i, i);
        for(int i = 0; i < 10; i ++)
        System.out.print(gai.get(i) + " ");
        System.out.println();
        try {
            Integer[] ia = gai.rep();
        } catch(Exception e) { System.out.println(e); }
    }
} /* Output: (Sample)
0 1 2 3 4 5 6 7 8 9
java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer;
*///:~

如今內部數組的呈現不是 T[] 而是 Object[],當 get() 被調用的時候數組的元素被轉型爲 T,這正是元素的實際類型。不過調用 rep() 仍是會報錯, 由於數組的實際類型依然是Object[],終究不能轉換爲其它類型。使用 Object[] 代替 T[] 的好處是讓咱們不會忘記數組運行期的實際類型,以致於不當心引入錯誤。

使用類型標識

其實使用 Class 對象做爲類型標識是更好的設計:

public class GenericArrayWithTypeToken<T> {
    private T[] array;
    @SuppressWarnings("unchecked")
    public GenericArrayWithTypeToken(Class<T> type, int sz) {
        array = (T[])Array.newInstance(type, sz);
    }
    public void put(int index, T item) {
        array[index] = item;
    }
    public T get(int index) { return array[index]; }
    // Expose the underlying representation:
    public T[] rep() { return array; }
    public static void main(String[] args) {
        GenericArrayWithTypeToken<Integer> gai =
        new GenericArrayWithTypeToken<Integer>(
        Integer.class, 10);
        // This now works:
        Integer[] ia = gai.rep();
    }
}

在構造器中傳入了 Class<T> 對象,經過 Array.newInstance(type, sz) 建立一個數組,這個方法會用參數中的 Class 對象做爲數組元素的組件類型。這樣建立出的數組的元素類型便再也不是 Object,而是 T。這個方法返回 Object 對象,須要把它轉型爲數組。不過其餘操做都不須要轉型了,包括 rep() 方法,由於數組的實際類型與 T[] 是一致的。這是比較推薦的建立泛型數組的方法。

總結

數組與泛型的關係仍是有點複雜的,Java 中不容許直接建立泛型數組。本文分析了其中緣由而且總結了一些建立泛型數組的方式。其中有部分我的的理解,若是錯誤但願你們指正。下一篇會總結通配符的使用,有興趣的讀者可進入下一篇:Java 泛型總結(三):通配符的使用

參考

  • Java 編程思想

若是個人文章對您有幫助,不妨點個贊支持一下(^_^)

相關文章
相關標籤/搜索