ArrayList 其實也有雙胞胎,但區別還是挺大的!

擁抱心中的夢想發表於2018-06-21

一、問題產生

今天在學習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$ArrayListtoArray()的實現方式:

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中將會呼叫ArrayscopyOf()方法,傳入一個newType進行型別判斷,newType的值為original.getClass(),由於original.getClass()返回的型別為Object[](具體看ArrayList的原始碼可知),所以呼叫toArray()之後將返回一個Object[]型別陣列,所以往listArray變數裡邊丟一個Object型別的物件當然不會報錯。

再看看java.util.Arrays$ArrayList的實現,可以看出資料型別定義為泛型EE的具體型別將根據你傳入的實際型別決定,由於你傳入了"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處卻報錯了,因為1fa變數的實際型別是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,兩個類的很多方法實現方式也不一樣。

謝謝閱讀,歡迎評論區交流!

相關文章