本文主要分成以下幾個部分:、
- 回顧在簡明數據結構中的異常
- 介紹Java中的協變和逆變的概念
JDK中的ArrayList
的異常
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652) ****
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
之前的文章中只說明了問題存在的一個方面(方法繼承和重寫方面的問題),其實這個問題存在的根本原因是Java中的數組是協變的。
協變和逆變的概念介紹
數組是協變的!
如果A
是 B
的子類,規定f()
是一種“變化”,比如強制類型轉換,使用集合類的泛型,數組等等,若f(A)
是 f(B)
的子類型,我們成A
可以協變為B
,否則就是逆變;如果這樣的變化后f(A)
和 f(B)
不能構成父子類型關系,我們稱之為不變。
協變在Java中的使用場景非常多,數組就是協變的,請看:
Object objects[] = new Integer[20];
我們可以很容易的將Integer
類型的數組變成Object
類型的數組,這兩種數組看起來就滿足了一種父類型和子類型的關系,這就是協變。但是這樣的“變化”( f()
)是具有風險的:
Object objects[] = new Integer[20];
objects[0] = "damn!";
Exception in thread "main" java.lang.ArrayStoreException: java.lang.String
at Main.main(Main.java:19)
objects
這個句柄實際上指向的是一個Integer
數組的引用,因此,不能存儲一個String
類型的字面量。上面的代碼雖然可以過編譯,但是在運行期會報錯。
泛型是不變的?
ArrayList<Object> list = new ArrayList<String>();
很明顯Object
是String
類型的父類型,但是這樣的代碼在編譯期間都不能成立。所以泛型不是協變的。
但是可以用通配符和泛型上下界的方法讓泛型具有協變和逆變的性質。
//協變
ArrayList< ? extends Number> list= new ArrayList<Integer>();
//逆變
ArrayList< ? super Integer> list2 = new ArrayList<Number>();
list.add(1); //error
這里的最后一行是錯誤的,直接會在編譯期報錯,這是因為通配符的語義是表示Number
的任意一種子類,可能是Float Double
等等,所以放入Integer
類型可能導致類型轉換錯誤。這里和Object
類型的語義完全不同之處在于這里沒有執行一個向下轉型操作。
ArrayList<Object> list3 = new ArrayList<>();
list3.add(1);
因此,使用上邊界標識符extends
的集合只能讀取集合中的信息:
ArrayList<? extends Number> list1 ;
ArrayList<Integer> list = new ArrayList<>(); list.add(1);
list1 = list;
list1.get(0);
相反下邊界標識符super
只能向集合中存儲東西,因為讀取出來的內容向下轉型是存在風險的
ArrayList<? super Integer> list3 = new ArrayList<>();
list3.add(0);
Integer i = list3.get(0);
JDK中的這個Bug的解釋
因為Java中的數組是協變的,所以真正存儲的類型不一定是Object
數組,所以再向數組中存儲內容是存在風險的。