前兩天給同事做code review,感覺自己對Java 的Generics 掌握得不夠好,便拿出《Effective Java》1 這本書再看看相關的章節。在Item 24:Eliminate unchecked warnings 這一節中,作者拿ArrayList 類中的public <T> T[] toArray(T[] a) 方法作為例子來說明如何對變量使用@SuppressWarnings annotation。
ArrayList 是一個generic class,它是這樣聲明的:
Javapublic class ArrayList<E> extends AbstractList<E>implements List<E>, RandomAccess, Cloneable, java.io.Serializable
這個類的toArray(T[] a) 方法是一個generic method,它是這樣聲明和實現的:
@SuppressWarnings("unchecked")public <T> T[] toArray(T[] a) {if (a.length < size)// Make a new array of a's runtime type, but my contents:return (T[]) Arrays.copyOf(elementData, size, a.getClass());System.arraycopy(elementData, 0, a, 0, size);if (a.length > size)a[size] = null;return a;}這個方法實際上是在Collection 接口中聲明的。因為我們經常通過ArrayList 使用它,這裡就用ArrayList 作為例子了。
1 為什麼聲明為不同類型?
我的問題是:為什麼這個方法使用類型T,而不使用ArrayList 的類型E ? 也就是說,這個方法為什麼不聲明成這樣:
Javapublic E[] toArray(E[] a);
如果類型相同的話,在編譯期間就可以發現參數的類型錯誤。如果類型不同,很容易產生運行時錯誤。比如下面這段代碼:
//創建一個類型為String 的ArrayListList<String> strList = new ArrayList<String>();strList.add("abc");strList.add("xyz");//將當前的strList 轉換成一個Number 數組。注意,下面的語句沒有任何編譯錯誤。 Number[] numArray = strList.toArray(new Number[0]);運行上面的代碼, Line 6 會拋出java.lang.ArrayStoreException 異常。
如果toArray 方法使用類型E 的話,語句2就會產生編譯錯誤。編譯錯誤怎麼說也比運行時錯誤親切啊。並且,generics 的主要目的就是為了類型安全,把類型轉換錯誤(ClassCastException)消滅在編譯期間。這個方法卻反其道而行之。難道這是一個大bug? Java 的bug 俺碰上過,但這個地方出bug 我還是不太敢相信。
上網一查,這個問題早已被討論過多次了2, 3, 4。
2 可以提高靈活性
這樣的聲明更靈活,可以把當前list 中的元素轉換成一個更一般類型的數組。比如,當前list 的類型是Integer,我們可以把它的元素轉換成一個Number 數組。
List<Integer> intList = new ArrayList<Integer>();intList.add(1);intList.add(2);Number[] numArray = intList.toArray(new Number[0]);
如果這個方法聲明成類型E,上面的代碼就會有編譯錯誤。 看起來,該方法聲明成下面這樣會更合適:
Javapublic <T super E> T[] toArray(T[] a);
不過, <T super E> 這樣的語法在Java 中是不存在的。而且即使存在,對數組也不起作用。也正是因為這個原因,在使用這個方法時,即使T 是E 的父類,或T 跟E 相同,也不能完全避免java.lang.ArrayStoreException 異常5, 6, 7 。請看下面兩段代碼。第一段代碼中T 是E 的父類,第二段代碼中T 和E 一樣。這兩段代碼都會拋出異常。
代碼一:
List<Integer> intList = new ArrayList<Integer>();intList.add(1);intList.add(2); Float[] floatArray = new Float[2];//Float 是Number 的子類,所以Float[] 是Number[] 的子類Number[] numArray = floatArray;//下面的語句會拋出ArrayStoreException 異常numArray = intList.toArray(numArray);
代碼二:
List<Number> intList = new ArrayList<Number>();//List 的類型是Number。但Number 是抽像類,只能存它的子類的實例intList.add(new Integer());intList.add(new Integer()); Float[] floatArray = new Float[];//Float 是Number 的子類,所以Float[] 是Number[] 的子類Number[] numArray = floatArray;//下面的語句會拋出ArrayStoreException 異常numArray = intList.toArray(numArray);
上面的異常都是由這個事實造成的:如果A 是B 的父類,那麼A[] 是B[] 的父類。 Java 中所有的類都繼承自Object,Object[] 是所有數組的父類。
這個帖子8裡舉了個例子,說明即使這個方法的類型聲明成E 也不能避免ArrayStoreException 異常。
該方法的文檔中也提到了這個異常:
ArrayStoreException if the runtime type of the specified array is not a supertype of the runtime type of every element in this list.
3 可以與Java 1.5 之前的版本兼容
這個方法在Java 引入Generics 之前(JDK1.5 中引入了Generics)就出現了9。那時它被聲明稱這樣:
Javapublic Object[] toArray(Object[] a)
Generics 出現後,許多類和方法就變成generic 的了。這個方法也隨大流聲明成這樣:
Javapublic <T> T[] toArray(T[] a)
這樣聲明可以與Java 1.5 之前的版本兼容10。
4 多嗦兩句
這個方法需要一個數組參數。如果這個數組的length 大於或等於當前list 的size,list 中的元素就會存儲到這個數組當中;如果這個數組的length 小於當前list 的size,就會創建一個新的數組,並把當前list 中的元素存入到這個新創建的數組中。為提高效率,如果可能,傳入的數組的length 要大於或等於list 的size,以避免該方法新建數組。
List<Integer> intList = new ArrayList<Integer>();intList.add();intList.add();//傳入一個數組,它的長度為Number[] numArray = intList.toArray(new Number[]); //語句//傳入一個數組,它的長度與intList 的長度相等Number[] numArray = intList.toArray(new Number[intList.size()]); //語句
另外,作為參數的數組不能為null ,否則的話會拋出NullPointerException 異常。
Footnotes:
1
Effective Java (2nd Edition)
2
Link
3
Link
4
Link
5
Link
6
Link
7
Link
8
Link
9
Link
10
Link
Created: 2016-04-06 Wed 21:14
Emacs 24.5.1 (Org mode 8.2.10)
Validate
以上內容是小編給大家介紹的Java ArrayList.toArray(T[]) 方法的參數類型是T 而不是E的原因分析,希望對大家有所幫助!