一、問題產生
今天在學習ArrayList
原始碼的時候發現了這麼一句註釋,即:
c.toArray might (incorrectly) not return Object[] (see 6260652)
這句話的意思是Collection
集合型別的toArray()
方法雖然宣告返回值型別是Object[]
,但是具體呼叫時還真不一定就返回Onject[]
型別,也有可能是其他的型別,這還要取決於你c
的實際型別,使用不當還會丟擲異常。這樣講可能會很懵比,下面我將會詳細講解到底為什麼,現在我們先來看看Collection
中的toArray()
宣告,讓你對這個方法先有個大概的印象。
public Object[] toArray(); // 宣告返回值型別為Object[]
複製程式碼
那麼什麼情況會出現上面的bug呢?我們先來看看下面兩個例子:
1、沒有拋異常的情況
// 宣告一個ArrayList集合,泛型為String型別
List<String> list = new ArrayList<>();
// 新增一個元素
list.add("list");
// 將上面的集合轉換為物件陣列
Object[] listArray = list.toArray(); ................ 1
// 輸出listArray的型別,輸出class [Ljava.lang.Object;
System.out.println(listArray.getClass());
// 往listArray賦值一個Onject型別的物件
listArray[0] = new Object();
複製程式碼
2、拋異常的情況
// 同一建立一個列表,但是現在是通過Arrays工具類來建立,建立的列表型別為Arrays的內部類ArrayList型別
List<String> asList = Arrays.asList("string");
// 轉換為物件陣列
Object[] asListArray = asList.toArray();.............. 2
// 輸出轉換後元素型別,將輸出class [Ljava.lang.String;
System.out.println(asListArray.getClass());
// 往物件陣列中新增Object型別物件,會報錯java.lang.ArrayStoreException
asListArray[0] = new Object();
複製程式碼
上面第一種情況是通過new ArrayList()
方式建立的java.util.ArrayList
型別,第二種方式是使用Arrays.asList()
方式建立的java.util.Arrays$ArrayList
的型別,兩個型別名都是ArrayList
,但實現方式確實不同的。那為什麼會報錯呢?歸根到底就是toArray()
這個方法的實現方式不同導致的。我們分別先看下java.util.ArrayList
類的toArray()
和java.util.Arrays$ArrayList
的toArray()
的實現方式:
java.util.ArrayList
public Object[] toArray() {
// 呼叫Arrays工具類進行陣列拷貝
return Arrays.copyOf(elementData, size);...............1
}
複製程式碼
Arrays.copyOf()
public static <T> T[] copyOf(T[] original, int newLength) {
return (T[]) copyOf(original, newLength, original.getClass());.................2
}
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
// 在建立新陣列物件之前會先對傳入的資料型別進行判定
@SuppressWarnings("unchecked")
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
複製程式碼
下面是java.util.Arrays$ArrayList
的實現
private final E[] a;
@Override
public Object[] toArray() {
return a.clone();
}
複製程式碼
從上面可以看出,在java.util.ArrayList
中將會呼叫Arrays
的copyOf()
方法,傳入一個newType
進行型別判斷,newType
的值為original.getClass()
,由於original.getClass()
返回的型別為Object[]
(具體看ArrayList
的原始碼可知),所以呼叫toArray()
之後將返回一個Object[]
型別陣列,所以往listArray
變數裡邊丟一個Object
型別的物件當然不會報錯。
再看看java.util.Arrays$ArrayList
的實現,可以看出資料型別定義為泛型E
,E
的具體型別將根據你傳入的實際型別決定,由於你傳入了"string"
,所以實際型別就是String[]
,當然呼叫a.clone()
之後還是一樣返回String[]
型別,只不過是這裡做了一個向上轉型,將String[]
型別轉為Object[]
型別返回罷了,但是注意,雖然返回的引用
為Object[]
,但實際的型別還是String[]
,當你往一個引用
型別和實際型別不匹配的物件中新增元素時,就是報錯。不服?下面舉個例子:
// 陣列strings為String[]型別
String[] strings = { "a", "b" };
// 向上轉型為Object[]型別,那麼這個objects就屬於引用型別為Object[],而實際型別為String[]
Object[] objects = strings;
// 新增一個Object型別變數,就報錯啦!
objects[0] = new Object();//! java.lang.ArrayStoreException
複製程式碼
為了加深理解,我們來總結下java中的向上轉型和向下轉型的區別。我們都知道我們可以通過注入Father fa = new Son()
的方式進行宣告,僅為Father
型別為Son
型別的父類,即發生向上轉型,向上轉型在java中是自動完成的,不需要進行強制轉換,不會丟擲異常。向下轉型分為兩種情況,下面結合程式碼演示:
// 向上轉型
Father fa = new Son();
Father fafa = new Father();
// 向下轉型(不會報錯)
Son son = (Son) fa;.................1
// 向下轉型,報錯了java.lang.ClassCastException
Son sonson = (Son) fafa;.......................2
複製程式碼
可以發現1
處不會報錯,2
處卻報錯了,因為1
處fa
變數的實際型別是Son
,引用型別為Father
,向下轉換取決於實際型別而不取決於引用型別,比如fafa
這個變數的實際型別就是其本身Father
,在java中,父類預設是不能強制轉換為子類的。
二、總結
首先最重要有以下幾點:
-
1、Java中陣列集合向上轉型之後,不能往陣列集合中新增引用型別(即父型別)的物件,而應該新增實際型別的物件,比如說``Father[] father = son[]
,你就不能往
father中新增
Father型別了,而應該是
Son` -
2、Java中向上轉型是預設允許的,但是向下轉型可能會丟擲錯誤,得小心使用!
-
3、要小心採用
Arrays.asList()
建立的集合型別不是java.util.ArrayList
,而是java.util.Arrays$ArrayList
,兩個類的很多方法實現方式也不一樣。
謝謝閱讀,歡迎評論區交流!