在將ArrayList等Collection轉為數組時,函數的返回值並不是泛型類型的數組,而是Object[]。剛好最近翻了一遍《java核心技術》,以及參考《Think in Java》,寫寫為什么沒有直接返回對應類型的數組,以及Java泛型中類型擦除的處理方式。
主要涉及:
- ArrayList的toArray函數使用
- 為什么不直接定義函數 T[] toArray()
- 泛型數組的創建的兩種常用方法
- 在泛型中創建具體的類實例
(部分代碼沒有運行過)
ArrayList的toArray函數使用
將ArrayList轉為數組,提供了兩個函數
Object[] toArray();
<T> T[] toArray(T[] a);
// 后面考慮一個Integer類型的ArrayList ArrayList<Integer> aa = new ArrayList<>();
aa.add(1);
aa.add(3);
Object[] toArray();
第一個函數是直接將ArrayList轉換成Object的數組,可以用Object[] bb = aa.toArray()
,在具體使用時對每個對象進行強制類型轉換,如System.out.println((Integer)bb[1])
。(java不支持數組之間的強制類型轉換)
T[] toArray(T[] a);
第二個函數能夠直接得到T類型的數組,當傳入的T[] a
能放下ArrayList時,會將ArrayList中的內容復制到a中(a的size較大時會a[size]=null)。否則,將構建一個新的數組並返回。具體實現如下:
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;
}
對於第二個函數,可以考慮將一個大小一致的T[]數組傳入toArray()函數(為了數組復用),或者直接Integer[] ArrayAA = aa.toArray(new Integer[0]);
。
為什么不直接定義函數 T[] toArray();
通常,直觀上更直接的返回數組的方式應該是T[] toArray()
,為什么JDK定義了一個不怎么好用的返回Object數組的函數。
數組之間雖然占用空間大小相同,但是不能強制改變類型(由於數組也是類,而數組類之間沒有繼承關系)。以object[] a; ...; (Integer[])a
強制轉換一個數組類型時,會在編譯器產生警告,運行時拋出異常。因此對於泛型數組,無法以(T[]) array
的形式,將擦除Object類型的數組強轉為T[]類型。
主要和jdk向前兼容以及泛型的類型擦除有關,個人認為主要應該還是由於類型擦除機制導致了返回T[] toArray()的實現困難。
泛型的類型擦除
泛型是從SE 5才開始引入,為了不破壞現有的類型機制,用了一種類型擦除的機制,相比C++使類型擦除時的考慮更為復雜。
虛擬機並不支持泛型,而是將泛型類編譯成了一個類型擦除(erased)的類,將類型變量轉換成一個原始類型(raw type)。原始類型在默認類型變量時會被轉換成Object,在類型變量有限定時(如 )會被轉換成限定的類。在運行時獲取到的T類型都是擦除后的類型。
public class Pair<T> {
private T first;
private T second;
public Pair(T first, T second){ this.first = first; this.second = second; }
}
// 會被替換成 public class Pair {
private Object first;
private Object second;
public Pair(Object first, Object second){
this.first = first;
this.second = second;
System.out.println(this.first.getClass()); // 不管T類型如何,得到的都是Object }
}
//當類型為Pair<T extends Comparable>時,T會被替換為Comparable
這和C++的處理方式很不一樣,C++中每個模板的實例化都會產生不同的具體類型,相當於對與每一種類型都會編譯出一套獨立的代碼,會有“模板代碼膨脹”。而在java中,使用了模板的類作為一個通用類進行了編譯,傳入不同的泛型參數也只會運行在同一個類上,模板的類型使用擦除后的類型進行編譯。
在使用到具體的對象時,編譯器會添加一個強制類型的轉換指定,將Object或限定的類型強轉為具體的類型。如對於類成員函數 public T getFirst()
,由於類型擦除后函數會變為public Object getFirst()
,當泛型T為整型時,編譯器調用 Int a = pair1.getFirst()
會添加一個強制類型轉換指令給虛擬機。而在沒有具體類型時,一直使用擦除后的類型進行處理。
泛型方法不涉及類型擦除
public <T> void f(T x){
System.out.println(x.getClass().getName());
}
f.(""); // java.lang.String f.(1); // java.lang.Integer
對於泛型方法,使用的是類型推斷機制,當調用方法時,通過參數判斷T的類型,而非擦除為Object。
<T> T[] toArray(T[] a);
函數就是通過這一方式,在調用toArray函數時通過參數類型得到泛型的類型,然后通過反射創建數組。
類型擦除導致的結果
由於類型的擦除,在使用時需要一直注意類型變量的類型並非T,編譯期無法得到關於T類型的具體信息,在運行時的類型並不會替換為具體的類型,而是在需要的地方執行強制類型轉換。 在運行時會出現下面的情況:
- 類型List和List的類型在擦除后相同。
- 同上 instanceOf 也無法使用。
T a = new T();
編譯器會報錯,因為類型在編譯期不存在,而且編譯階段無法確定在T中是否存在默認的無參構造函數。- 同上,無法使用
T[] a = new T[10]
。
外加數組類之間無繼承關系導致無法將Object[]的數組強轉為T[]。
因此,java中直接設計T[] toArray()
類型的函數需要額外的傳入類型。
泛型數組的創建的兩種常用方法
雖然無法直接創建T類型的對象,但可以利用反射機制間接的創建T類型的對象。對於創建泛型數組,一般的方案是使用ArrayList。如果某些情況下需要自己實現,可以使用和ArrayList類似的方式。
1、JDK通過創建Object[]的數組放對象,在取對象時進行類型轉換,此時toArray函數通過泛型函數的參數獲取類型。
// 數組仍使用Object類型 private Object[] array = new Object[size];
// 在get函數中強制類型轉換 public T get(int index){
return (T)array[index];
}
// 轉換成數組 public T[] toArray(T[] a){
// 此處a只用於獲取類型 // 更嚴謹的實現參考上面的JDK代碼 return (T[]) Arrays.copyOf(elementData, size, a.getClass());
}
2、或者傳入具體的類型,由於傳入的具體類型可以創建具體類型數組,因此可以直接實現T[] toArray()
。可能是傳入類型的方式不太優雅,JDK並沒有使用這種形式。
class GenericArray{
private T[] array;
// 構造函數直接傳入類型,數組的強制類型轉換會產生編譯警告,此處直接用標簽忽略 @SuppressWarnings("unchecked")
public GenericArray(Class<T> type, int size){
array = (T[]) Array.newInstance(type, size);
}
public T[] toArray(){
return array;
}
}
在泛型中創建具體的類實例
和上面的情況類似,要想在泛型類中創建具體的類型,也就是需要在類中能夠得到T.class
,通常需要使用兩種方式:
- 將
T.class
通過函數或其它方式傳入類中,通過反射機制創建。 - 泛型函數能夠從參數的類型中獲取
T.class
。
后面簡單介紹構造函數包裝后傳入的方式。
通過構造函數傳入類型后創建類實例
對於T a = new T();
,由於類型擦除無法創建,但可以通過在運行時傳入類變量來實現創建,將類型通過構造函數傳入。在有類型后,通過反射機制(newInstance)構建新的類。
public class ClassAsFactory<T>{
Class<T> kind;
public ClassAsFactory(Class<T> kind){ this.kind = kind; }
// 構建時傳入 String.class public static void main(String[] argvs){
ClassAsFactory<String> gClass = new ClassAsFactory<String>(String.class);
}
}
但是對於這段代碼,編譯器無法檢查構造函數是否存在等問題,一般更建議使用顯示類型工廠,在構造函數中傳入new過具體類型的工廠類:
Interface FactoryI<T>{
T create();
}
// 在工廠類中傳入具體的對象 Class IntegerFactory implements FactoryI<Integer>{
public Integer Create() { return new Interger(0);}
}
Class Foo2<T> {
private T x;
// 類型F用來限制參數為工廠類 public <F extends FactoryI<T>> Foo2(F factory){
x = factory.create();
}
public static void main(String[] argvs){
new Foo2<Integer>(new IntegerFactory());
}
此時,具體工廠類由於針對具體的類型,編譯期間可以對創建過程進行檢查。
《Think in Java》里還提到一種模板方法設計模式,沒有太大的本質上的區別。