java從toArray返回Object[]到泛型的類型擦除


本文通過MetaWeblog自動發布,原文及更新鏈接:https://extendswind.top/posts/technical/java_toarray_return_and_generic_type_erase

在將ArrayList等Collection轉為數組時,函數的返回值並不是泛型類型的數組,而是Object[]。剛好最近翻了一遍《java核心技術》,以及參考《Think in Java》,寫寫為什么沒有直接返回對應類型的數組,以及Java泛型中類型擦除的處理方式。

主要涉及:

  1. ArrayList的toArray函數使用
  2. 為什么不直接定義函數 T[] toArray()
  3. 泛型數組的創建的兩種常用方法
  4. 在泛型中創建具體的類實例

(部分代碼沒有運行過)

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,通常需要使用兩種方式:

  1. T.class通過函數或其它方式傳入類中,通過反射機制創建。
  2. 泛型函數能夠從參數的類型中獲取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》里還提到一種模板方法設計模式,沒有太大的本質上的區別。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM