陣列與泛型的關係還是有點複雜的,Java 中不允許直接建立泛型陣列。本文分析了其中原因並且總結了一些創建泛型數組的方式。具有很好的參考價值。下面跟著小編一起來看下吧
簡介
上一篇文章介紹了泛型的基本用法以及類型擦除的問題,現在來看看泛型和數組的關係。陣列比起Java 類別函式庫中的容器類別是比較特殊的,主要體現在三個方面:
陣列建立後大小便固定,但效率更高
陣列能追蹤它內部保存的元素的具體類型,插入的元素類型會在編譯期間得到檢查
陣列可以持有原始型別( int,float等),不過有了自動裝箱,容器類別看上去也能持有原始型別了
那麼當數組遇到泛型會怎樣? 能否創建泛型數組呢?這是這篇文章的主要內容。
這個系列的另外兩篇文章:
Java 泛型總結(一):基本用法與型別擦除
Java 泛型總結(三):通配符的使用
#泛型陣列
如何建立泛型數組
如果有一個類別如下:
class Generic<T> { }
如果要建立一個泛型數組,應該是這樣:Genericaa5a6f1eae9aaeb331563a5057ed6ec2[]
不過行程式碼會報錯,也就是說不能直接建立泛型陣列。
那麼如果要使用泛型陣列怎麼辦?一種方案是使用 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 Genericc0f559cc8d56b43654fcbe4aa9df7b4a[]
具體參見下面的例子:
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 = (Genericc0f559cc8d56b43654fcbe4aa9df7b4a[])new Object[SIZE]
,陣列在建立的時候是一個Object 數組,如果轉型便會報錯誤。成功建立泛型數組的唯一方式是建立一個類型擦除的數組,然後轉型,如程式碼: gia = (Genericc0f559cc8d56b43654fcbe4aa9df7b4a[])new Generic[SIZE]
,gia 的Class 物件輸出的名字是Generic[]。
我個人的理解是:由於類型擦除,所以Genericc0f559cc8d56b43654fcbe4aa9df7b4a 相當於初始類型Generic,那麼gia = (Genericc0f559cc8d56b43654fcbe4aa9df7b4a[])new Generic[SIZE]
中的轉型其實還是轉型為Generic[],看上去像沒轉,但是多了編譯器對參數的檢查和自動轉型,向數組插入new Object()
和new Genericb9bd724a0d28d703d062252edbd76b9b()
皆會報錯,而gia[0] 取出給Genericc0f559cc8d56b43654fcbe4aa9df7b4a
也不需要我們手動轉型。
使用 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[]
,看上去就是沒轉,但是多了編譯器的參數檢查和自動轉型。而如果把泛型參數改成ba95df6fbb265e67dd0723036aced316
,那麼因為型別是擦除到第一個邊界,所以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(); } }
比起原始的版本,上面的程式碼只修改了第一行,把 8742468051c85b06f0a0af9e3e506b5c
改成了ba95df6fbb265e67dd0723036aced316
那麼不用呼叫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(); } }
在构造器中传入了 Class8742468051c85b06f0a0af9e3e506b5c
对象,通过 Array.newInstance(type, sz)
创建一个数组,这个方法会用参数中的 Class 对象作为数组元素的组件类型。这样创建出的数组的元素类型便不再是 Object,而是 T。这个方法返回 Object 对象,需要把它转型为数组。不过其他操作都不需要转型了,包括 rep() 方法,因为数组的实际类型与 T[] 是一致的。这是比较推荐的创建泛型数组的方法。
总结
数组与泛型的关系还是有点复杂的,Java 中不允许直接创建泛型数组。本文分析了其中原因并且总结了一些创建泛型数组的方式。其中有部分个人的理解,如果错误希望大家指正。下一篇会总结通配符的使用。
以上是Java泛型總結(二)-泛型與陣列的詳細內容。更多資訊請關注PHP中文網其他相關文章!