15泛型_15.8擦除的補償

15.8 擦除的補償

正如我們看到的,擦除丟失了在泛型代碼中執行某些操作的能力。任何在運行時需要知道確切類型信息的操作都將無法工作:

//: generics/Erased.java
// {CompileTimeError} (Won't compile)

public class Erased<T> {
  private final int SIZE = 100;
  public static void f(Object arg) {
    if(arg instanceof T) {}          // Error
    T var = new T();                 // Error
    T[] array = new T[SIZE];         // Error
    T[] array = (T)new Object[SIZE]; // Unchecked warning
  }
} ///:~

偶爾可以繞過這些問題來編程,但是有時必須通過引入類型標籤來對擦除進行補償。這意味着你需要顯式地傳遞你的類型的Class對象,以便你可以在類型表達式中使用它。

例如,在前面示例中對使用instanceof的嘗試最終失敗了,因爲其類型信息已經被擦除了。如果引入類型標籤,就可以轉而使用動態的isInstance():

//: generics/ClassTypeCapture.java

class Building {}
class House extends Building {}

public class ClassTypeCapture<T> {
  Class<T> kind;
  public ClassTypeCapture(Class<T> kind) {
    this.kind = kind;
  }
  public boolean f(Object arg) {
    return kind.isInstance(arg);
  } 
  public static void main(String[] args) {
    ClassTypeCapture<Building> ctt1 =
      new ClassTypeCapture<Building>(Building.class);
    System.out.println(ctt1.f(new Building()));
    System.out.println(ctt1.f(new House()));
    ClassTypeCapture<House> ctt2 =
      new ClassTypeCapture<House>(House.class);
    System.out.println(ctt2.f(new Building()));
    System.out.println(ctt2.f(new House()));
  }
} /* Output:
true
true
false
true
*///:~

編譯器將確保類型標籤可以匹配泛型參數。

15.8.1 創建類型實例

在Erased.java中對創建一個new T()的嘗試將無法實現,部分原因是因爲擦除,而另一部分原因是因爲編譯器不能驗證T具有默認(無參)構造器。但是在C++中,這種操作很自然、很直觀,並且很安全(它是在編譯期受到檢查的):

//: generics/InstantiateGenericType.cpp
// C++, not Java!

template<class T> class Foo {
  T x; // Create a field of type T
  T* y; // Pointer to T
public:
  // Initialize the pointer:
  Foo() { y = new T(); }
};

class Bar {};

int main() {
  Foo<Bar> fb;
  Foo<int> fi; // ... and it works with primitives
} ///:~

Jaya中的解決方案是傳遞一個工廠對象,並使用它來創建新的實例。最便利的工廠對象就是Class對象,因此如果使用類型標籤,那麼你就可以使用newInstance來創建這個類型的新對象:

//: generics/InstantiateGenericType.java
import static net.mindview.util.Print.*;

class ClassAsFactory<T> {
  T x;
  public ClassAsFactory(Class<T> kind) {
    try {
      x = kind.newInstance();
    } catch(Exception e) {
      throw new RuntimeException(e);
    }
  }
}

class Employee {}   

public class InstantiateGenericType {
  public static void main(String[] args) {
    ClassAsFactory<Employee> fe =
      new ClassAsFactory<Employee>(Employee.class);
    print("ClassAsFactory<Employee> succeeded");
    try {
      ClassAsFactory<Integer> fi =
        new ClassAsFactory<Integer>(Integer.class);
    } catch(Exception e) {
      print("ClassAsFactory<Integer> failed");
    }
  }
} /* Output:
ClassAsFactory<Employee> succeeded
ClassAsFactory<Integer> failed
*///:~

這可以編譯,但是會因ClassAsFactory<Integer>而失敗,因爲Integer沒有任何默認的構造器。因爲這個錯誤不是在編譯期捕獲的,所以Sun的夥計們對這種方式井不贊成,他們建議使用顯式的工廠,井將限制其類型,使得只能接受實現了這個工廠的類:

//: generics/FactoryConstraint.java

interface FactoryI<T> {
  T create();
}

class Foo2<T> {
  private T x;
  public <F extends FactoryI<T>> Foo2(F factory) {
    x = factory.create();
  }
  // ...
}

class IntegerFactory implements FactoryI<Integer> {
  public Integer create() {
    return new Integer(0);
  }
}   

class Widget {
  public static class Factory implements FactoryI<Widget> {
    public Widget create() {
      return new Widget();
    }
  }
}

public class FactoryConstraint {
  public static void main(String[] args) {
    new Foo2<Integer>(new IntegerFactory());
    new Foo2<Widget>(new Widget.Factory());
  }
} ///:~

注意,這確實只是傳遞Class<T>的一種變體。兩種方式都傳遞了工廠對象,Class<T>碰巧是內建的工廠對象,而上面的方式創建了一個顯式的工廠對象,但是你卻獲得了編譯期檢查。

另一種方式是模板方法設計模式。在下面的示例中,get()是模板方法,而create()是在子類中定義的、用來產生子類類型的對象:

//: generics/CreatorGeneric.java

abstract class GenericWithCreate<T> {
  final T element;
  GenericWithCreate() { element = create(); }
  abstract T create();
}

class X {}

class Creator extends GenericWithCreate<X> {
  X create() { return new X(); }
  void f() {
    System.out.println(element.getClass().getSimpleName());
  }
}   

public class CreatorGeneric {
  public static void main(String[] args) {
    Creator c = new Creator();
    c.f();
  }
} /* Output:
X
*///:~

15.8.2泛型數組

正如你在Erased.java中所見,不能創建泛型數組。一般的解決方案是在任何想要創建泛型數組的地方都使用ArrayList:

//: generics/ListOfGenerics.java
import java.util.*;

public class ListOfGenerics<T> {
  private List<T> array = new ArrayList<T>();
  public void add(T item) { array.add(item); }
  public T get(int index) { return array.get(index); }
} ///:~

這裏你將獲得數組的行爲,以及由泛型提供的編譯期的類型安全。

有時,你仍舊希璧創建泛型類型的數組(例如,ArrayList內部使用的是數組)。有趣的是可以按照編譯器喜歡的方式來定義一個引用,例如:

//: generics/ArrayOfGenericReference.java

class Generic<T> {}

public class ArrayOfGenericReference {
  static Generic<Integer>[] gia;
} ///:~

編譯器將接受這個程序,而不會產生任何警告。但是,永遠都不能創建這個確切類型的數組(包括類型參數),因此這有一點令人困惑。既然所有數組無論它們持有的類型如何,都具有相同的結構(每個數組槽位的尺寸和數組的佈局),那麼看起來你應該能夠創建一個Object數組,並將其轉型爲所希望的數組類型。事實上這可以編譯,但是不能運行,它將產生ClassCaseException:

//: generics/ArrayOfGeneric.java

public class ArrayOfGeneric {
  static final int SIZE = 100;
  static Generic<Integer>[] gia;
  @SuppressWarnings("unchecked")
  public static void main(String[] args) {
    // Compiles; produces ClassCastException:
    //! gia = (Generic<Integer>[])new Object[SIZE];
    // Runtime type is the raw (erased) type:
    gia = (Generic<Integer>[])new Generic[SIZE];
    System.out.println(gia.getClass().getSimpleName());
    gia[0] = new Generic<Integer>();
    //! gia[1] = new Object(); // Compile-time error
    // Discovers type mismatch at compile time:
    //! gia[2] = new Generic<Double>();
  }
} /* Output:
Generic[]
*///:~

問題在於數組將跟蹤它們的實際類型,而這個類型是在數組被創建時確定的,因此,即使gia已經被轉型爲Generic<Integer>[],但是這個信息只存在幹編譯期(如果沒有@Suppress Warnings註解,你將得到有關這個轉型的警告)。在運行時,它仍將引發問題。成功創建泛型數組的唯一方式就是創建一個被擦除類型的新數組,然後對其轉型。

讓我們看一個更復雜的示例。考慮一個簡單的泛型數組包裝器:

//: generics/GenericArray.java

public class GenericArray<T> {
  private T[] array;
  @SuppressWarnings("unchecked")
  public GenericArray(int sz) {
    array = (T[])new Object[sz];
  }
  public void put(int index, T item) {
    array[index] = item;
  }
  public T get(int index) { return array[index]; }
  // Method that exposes the underlying representation:
  public T[] rep() { return array; }    
  public static void main(String[] args) {
    GenericArray<Integer> gai =
      new GenericArray<Integer>(10);
    // This causes a ClassCastException:
    //! Integer[] ia = gai.rep();
    // This is OK:
    Object[] oa = gai.rep();
  }
} ///:~

與前面相同,我們井不能聲明T[] array=new T[sz],因此我們創建了一個對象數組,然後將其轉型。
rep()方法將返回T[],它在main()中將用於gai,因此應該是Integer[],但是如果調用它,並嘗試着將結果作爲Integer[]引用來捕獲,就會得到ClassCastException,這還是因爲實際的運行時類型是Objet[]。

如果在註釋掉@Suppresswarniugs註解之後再編譯GenericArray.java ,編譯器就會產生警告:

Note: GenericArray.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.

在這種情況下,我們將只獲得單個的警告,並且相信這事關轉型。但是如果真的想要確定是否是這麼回事,就應該用.Xlint:unchecked來編譯:

GenerieArray.java:7: warning:[unchecked] unchecked cast
found : java.lang.Object[]
required: T[]
array = (T[])new Object[sz];
                  ^                      
1 warning

這確實是對轉型的抱怨。因爲警告會變得令人迷惑,所以一旦我們驗證某個特定警告是可預期的,那麼我們的上策就是用@SuppressWarnings關閉它。通過這種方式,當警告確實出現時,我們就可以真正地展開對它的調查了。

因爲有了擦除,數組的運行時類型就只能是Object[]。如果我們立即將其轉型爲T[],那麼在編譯期該數組的實際類型就將丟失,而編譯器可能會錯過某些潛在的錯誤檢查。正因爲這樣,最好是在集合內部使用Object[],然後當你使用數組元素時,添加一個對T的轉型。讓我們看着這是如何作用於GenericArray.java示例的:

//: generics/GenericArray2.java

public class GenericArray2<T> {
  private Object[] array;
  public GenericArray2(int sz) {
    array = new Object[sz];
  }
  public void put(int index, T item) {
    array[index] = item;
  }
  @SuppressWarnings("unchecked")
  public T get(int index) { return (T)array[index]; }
  @SuppressWarnings("unchecked")
  public T[] rep() {
    return (T[])array; // Warning: unchecked cast
  } 
  public static void main(String[] args) {
    GenericArray2<Integer> gai =
      new GenericArray2<Integer>(10);
    for(int i = 0; i < 10; i ++)
      gai.put(i, i);
    for(int i = 0; i < 10; i ++)
      System.out.print(gai.get(i) + " ");
    System.out.println();
    try {
      Integer[] ia = gai.rep();
    } catch(Exception e) { System.out.println(e); }
  }
} /* Output: (Sample)
0 1 2 3 4 5 6 7 8 9
java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer;
*///:~

初看起來,這好像沒多大變化,只是轉型挪了地方。如果沒有@Suppresswarnings註解,你仍舊會得到unchecked告。但是,現在的內部表示是Object[]而不是T[]。當get()被調用時,它將對象轉型爲T,這實際上是正確的類型,因此這是安全的。然而,如果你調用rep() ,它還是嘗試着將Object[]轉型爲T[],這仍舊是不正確的,將在編譯期產生警告,在運行時產生異常。因此,沒有任何方式可以推翻底層的數組類型,它只能是Object[]。在內部將array當作Object[]而不是T[]處理的優勢是:我們不太可能忘記這個數組的運行時類型,從而意外地引入缺陷(儘管大多數也可能是所有這類缺陷都可以在運行時快速地探測到)。

對於新代碼,應該傳遞一個類型標記。在這種情況下,GenericArray看起來會像下面這樣:

//: generics/GenericArrayWithTypeToken.java
import java.lang.reflect.*;

public class GenericArrayWithTypeToken<T> {
  private T[] array;
  @SuppressWarnings("unchecked")
  public GenericArrayWithTypeToken(Class<T> type, int sz) {
    array = (T[])Array.newInstance(type, sz);
  }
  public void put(int index, T item) {
    array[index] = item;
  }
  public T get(int index) { return array[index]; }
  // Expose the underlying representation:
  public T[] rep() { return array; }    
  public static void main(String[] args) {
    GenericArrayWithTypeToken<Integer> gai =
      new GenericArrayWithTypeToken<Integer>(
        Integer.class, 10);
    // This now works:
    Integer[] ia = gai.rep();
  }
} ///:~

類型標記Class<T>被傳遞到構造器中,以便從擦除中恢復,使得我們可以創建需要的實際類型的數組,儘管從轉型中產生的警告必須用@Suppresswarnings壓制住。一旦我們獲得了實際類型。就可以返回它,並獲得想要的結果,就像在main()中看到的那樣。該數組的運行時類型是確切類型T[]。

遺憾的是,如果查看java SE5標準類庫中的源代碼,你就會看到從Object數組到參數化類型的轉型遍及各處。例如,下面是經過整理和簡化之後的從Collection中複製ArrayList的構造器:

public ArrayList(Collection c) {
    size = c.size();
    elementData = (E[])new Object[size];
    c.toArray(elementData);
}

如果你通讀ArrayList.java,就會發現它充滿了這種轉型。如果我們編譯它,又會發生什麼呢?

Note: ArrayList.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.

可以十分肯定,標準類庫會產生大量的警告。如果你曾經用過C++,特別是ANSI C之前的版本,你就會記得警告的特殊效果:當你發現可以忽略它們時,你就可以忽略。正是因爲這個原因,最好是從編譯器中不要發出任何消息,除非程序員必須對其進行響應。

Neal Gafter (Java SE5的領導開發者之一)在他的博客中指出,在重寫Java庫時,他十分懶散,而我們不應該像他那樣。Neal還指出,在不破壞現有接口的情況下,他將無法修改某些Java類庫代碼。因此,即使在Java類庫源代碼中出現了某些慣用法,也不能表示這就是正確的解決之道。當查看類庫代碼時,你不能認爲它就是應該在自己的代碼中遵循的示例。

發佈了39 篇原創文章 · 獲贊 3 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章