List vs Array:誰適合做java中泛型物件的容器?
在java中,我們知道有兩大類線性的資料結構是陣列(Array)和連結串列(主要是ArrayList和LinkedList)。對於一般的物件來說,我們可以選擇這兩類資料結構中的任何一種資料結構來儲存我們的物件。但是對於泛型物件,我們應該選擇哪種資料結構來儲存他們呢?答案是:List。那麼為什麼不選擇Array呢?具體的原因看我下面的分析:
首先我需要給出List和Array這兩個資料結構在java實現中的兩個重要的不同點
類別 |
不同點1 |
不同點2 |
Array |
Covariant(協變) |
Reified(具體化) |
List |
Invariant(不變) |
Erasure(擦除) |
下面我就來解釋一下這些術語的含義:
Covariant(協變):協變的本意是指一個變數發生了變化,那麼其它的一些變數也會跟著(協同)發生變化,從而保留一些數學特性。在java中也是同樣的意思,表示如果一個B型別是A型別的子類,那麼B型別宣告的陣列(B[])也是A型別宣告陣列(A[])的子類(注意這裡是變化了的,千萬不要認為是不變的)。
Invariant(不變性):和covariant剛好相反,容器中儲存的型別的繼承關係不會影響到容器本身。比如:B型別是A型別的子類,那麼ArrayList<B>並不是ArrayList<A>的子類,更不是它的父類,總之這兩個類什麼關係都沒有。
Reified(具體化):陣列對於其中儲存物件的型別的檢查是在執行時進行的。
Erasure(擦除):連結串列對於其中儲存物件的型別的檢查是在編譯時進行的,而在執行時的時候,它會自動“擦除”掉物件的型別。這樣做的目的是為了更好的與用低版本寫出來的程式相容,因為畢竟“泛型”是在JDK 1.5才引入的。
正因為這兩個不同點,賦予了List在儲存泛型物件的時候得天獨厚的優勢。而且在java中我們不能宣告擁有具體型別的泛型陣列。
我們假設A這個類是一個泛型類:
A<E>[] a = new A<E>[3]; // 不合法
A<Integer>[] a = new A<Integer>[3]; // 不合法
A[] a = new A[3]; // 合法,但是會有一個rawtypes的警告
如果我們想去除這個警告,有兩種解決方法:
1. @SuppressWarnings("rawtypes")
A[] a = new A[3];
2. A<?>[] a = new A<?>[2];
雖然這兩種方法都可以消除警告,但是都含有隱藏的風險,都有可能丟擲ClassCastException。那麼我們還有其他更好的解決方案嗎?答案是肯定的,就是用List來儲存泛型的物件:
// A是我們自定義的泛型類
ArrayList<A<Integer>> a = new ArrayList<>();
細心的讀者可能發現了,上面兩個不太好的解決方案有個共同的特這就是:沒有為泛型物件指定具體的資料型別,這樣就導致我們在寫程式碼的時候要特別的小心資料型別的轉換,因為這樣的陣列可以放入A<Integer>,A<String>,A<Object>...這些不同型別的物件。這樣一來我們就失去了使用泛型類的意義了,因為在java中使用泛型類就是為了避免我們做過多的資料型別檢查,而這樣一來,雖然我們用到了泛型類,但是還是要做資料型別的檢查。
那麼你們可能會問:如果在java中允許陣列中放具體型別的泛型物件,這個問題不就解決了嗎?是的,這個問題解決了,但是由於前面我們談到的陣列的兩個特性,有一個新的更加嚴重的問題就產生了:java程式碼會存在嚴重的安全隱患(在執行時很容易丟擲ClassCastException,這個異常其實在編譯時的時候就可以被java虛擬機器察覺的,但是一旦我們允許陣列中引入具體型別的泛型陣列,這個異常就不會被java虛擬機器覺察了,當然在執行的時候就容易出錯了)。
看下面一段程式碼,在這個程式碼中,我們假設java虛擬機器允許陣列儲存具體型別的泛型物件。
ArrayList<Integer>[] a = new ArrayList<Integer>[5]; // 假設這個是合法的,其實在java中不能這麼用
Object[] b = a; // 這個 語句肯定也是合法的,由於陣列的covariant
ArrayList<String> item1 = new ArrayList<String>();
item1.add("I am dangerous!!!");
b[0] = item1; // 這個語句也是合法的
Integer i = a[0].get(0); // 如果第一條語句合法,那麼這條語句肯定也沒有問題。但是如果這樣寫,我就是將一個字串型別的資料賦給整型變數,肯定會丟擲ClassCastException的。
從這個例子中我們可以發現,如果java執行陣列儲存具體型別的泛型物件,那麼最終可能會導致ClassCastException這個異常的丟擲。一般來說,ClassCastException這個異常風險java虛擬機器是可以發現的,尤其是對於泛型物件而言,但是這裡我們雖然用到了泛型物件,但是java虛擬機器是不可能發現這個風險的,除非我們執行最後一條語句。這樣我們就將這個風險放在了執行時,對於程式設計師的我們來說這是不能容忍的。我們肯定就會責怪java的設計者,所以java的設計者就一不做二不休,乾脆不讓陣列儲存具體型別的泛型物件。如果程式設計師非要在陣列中儲存泛型物件,那麼就不能指定具體型別,型別檢查就留給我們程式設計師自己去做,或者java虛擬機器會給出一個警告,這樣做的目的其實就是推卸責任,告訴程式設計師,後面程式中一旦出錯,就不關java的事情了,全是你們程式設計師自己的責任。
比如我將上面的程式碼稍微的改一下,變成java中可以執行的程式碼,但是第一行會有一個警告,整個程式依然會丟擲ClassCastException,但是有了上面的警告,java就將自己的“責任”成功的推給了程式設計師。
@SuppressWarnings("unchecked")
ArrayList<Integer>[] a = new ArrayList[5]; // 合法,有警告
Object[] b = a; // 這個語句肯定也是合法的
ArrayList<String> item1 = new ArrayList<String>();
item1.add("I am dangerous!!!");
b[0] = item1; // 這個語句也是合法的
Integer i = a[0].get(0); //我將一個字串型別的資料賦給整型變數,肯定會丟擲ClassCastException的。但是與第一個例子不同的是,這個風險java虛擬機器在編譯的時候已經發現了,所以才會給我們第一行的程式碼爆出一個警告。這樣一來,這個錯誤的出現就完全是我們程式設計師的責任了,和java虛擬機器沒有關係了,因為別個已經告訴了我們會有風險。
綜上所述,由於java中陣列的特性,我們不應該將泛型的物件放在陣列中儲存,而是應該放在List中儲存。比如上面的例子,如果我們需要儲存ArrayList這個泛型的物件,我們應該寫成:
ArrayList<ArrayList<Integer>> a = new ArrayList<>();
這樣就萬無一失了!!!