1. 程式人生 > >泛型遇上陣列

泛型遇上陣列

簡介

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

  1. 陣列建立後大小便固定,但效率更高

  2. 陣列能追蹤它內部儲存的元素的具體型別,插入的元素型別會在編譯期得到檢查

  3. 陣列可以持有原始型別 ( int,float等 ),不過有了自動裝箱,容器類看上去也能持有原始型別了

那麼當陣列遇到泛型會怎樣? 能否建立泛型陣列呢?這是這篇文章的主要內容。

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

泛型陣列

如何建立泛型陣列

如果有一個類如下:

 class Generic<T> {

}

如果要建立一個泛型陣列,應該是這樣: Generic<Integer> ga = new Generic<Integer>[]。不過行程式碼會報錯,也就是說不能直接建立泛型陣列。

那麼如果要使用泛型陣列怎麼辦?一種方案是使用 ArrayList,比如下面的例子:

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>[]。具體參見下面的例子:

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]gia 的 Class 物件輸出的名字是 Generic[]

我個人的理解是:由於型別擦除,所以 Generic<Integer> 相當於初始型別 Generic,那麼 gia = (Generic<Integer>[])new Generic[SIZE] 中的轉型其實還是轉型為 Generic[],看上去像沒轉,但是多了編譯器對引數的檢查和自動轉型,向陣列插入 new Object() 和 new Generic<Double>() 均會報錯,而 gia[0] 取出給 Generic<Integer> 也不需要我們手動轉型。

使用 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 中不允許直接建立泛型陣列。本文分析了其中原因並且總結了一些建立泛型陣列的方式。其中有部分個人的理解,如果錯誤希望大家指正。下一篇會總結萬用字元的使用。