Java編程思想__泛型(四)

邊界處的動作

  • 正是因爲有了擦除,我們發現泛型最令人困惑的方面源自這樣一個事實,即可以表示沒有任何意義的事物。
public class ArrayMaker<T> {

    private Class<T> tClass;

    public ArrayMaker(Class<T> tClass) {
        this.tClass = tClass;
    }

    public T[] create(int size){
        return (T[])Array.newInstance(tClass,size);
    }
    public static void main(String[] args) {
        ArrayMaker<String> arrayMaker=new ArrayMaker<>(String.class);
        String[] strings = arrayMaker.create(9);
        System.out.println(Arrays.toString(strings));
    }
}

//運行結果爲

[null, null, null, null, null, null, null, null, null]
  1. 即使 tClass 被存儲爲 Class<T> ,擦除也意味着它實際將被存儲爲 Class,沒有任何參數。因此, 當你正在使用它時,例如在創建數組 Array.newInstance() 實際上並未擁有 tclass所蘊含的類型信息,因此這不會產生具體的結果,所以必須轉型,這將產生一條令你無法滿意的警告。
  2. 注意, 對於在泛型中創建數組,使用Array.newInstance()是推薦的方式。
  3. 如果我們要創建一個容器而不是數組,情況就有些不同了:
public class ArrayMaker1<T> {
    List<T> create(){
        return new ArrayList<T>();
    }
    public static void main(String[] args) {
        ArrayMaker1<String> maker1 = new ArrayMaker1<>();
        List<String> strings = maker1.create();
        System.out.println(strings);
    }
}

//運行結果
[]
  1. 編譯器不會給出任何警告,儘管我們(從擦除中)知道在 create() 內部的 new ArrayList<T> 中的 <T> 被移除了 ___在運行時,這個類的內部沒有任何<T> ,因此這看起來毫無意義。但是如果你遵從這種思路,並將這個表達式改爲 new ArrayList() , 編譯器就會給出警告。
  2. 在本例中,這是否真的毫無意義呢? 如果返回list之前,將某些東西放入其中,就像下面這樣,情況又會如何呢?
public class ArrayMark2<T> {
    List<T> create(T t,int size){
        List<T> list=new ArrayList<>();
        for (int i = 0; i < size; i++) {
            list.add(t);
        }
        return list;
    }

    public static void main(String[] args) {
        ArrayMark2<String> arrayMark2=new ArrayMark2<>();
        List<String> list = arrayMark2.create("hello", 3);
        System.out.println(list);
    }
}
//運行結果爲

[hello, hello, hello]
  1. 即使編譯器無法知道有關 create() 中的 T 的任何信息, 但是它仍舊可以在編譯期確保你放置到 list 中的對象具有 T 類型,使其適合 ArrayList<T> 。
  2. 因此, 即使擦除在方法或類內部移除了有關實際類型的信息,編譯器仍舊可以確保在方法或類中使用的類型的內部一致性。
  3. 因爲擦除在方法體中移除了類型信息,所以在運行時的問題就是邊界: 即對象進入和離開的地點。這些正是編譯器在編譯器執行類型檢查並插入轉型代碼的地點。
public class SimpleHolder {
    private Object obj;

    public Object getObj() {
        return obj;
    }

    public void setObj(Object obj) {
        this.obj = obj;
    }
    public static void main(String[] args) {
        SimpleHolder holder = new SimpleHolder();
        holder.setObj("Item");
        String obj = (String) holder.getObj();
    }
}
  1. 用 javap -c 反編譯 SimpleHolder 類,得到如下內容
 public java.lang.Object getObj();
    Code:
       0: aload_0
       1: getfield      #2                  // Field obj:Ljava/lang/Object;
       4: areturn

  public void setObj(java.lang.Object);
    Code:
       0: aload_0
       1: aload_1
       2: putfield      #2                  // Field obj:Ljava/lang/Object;
       5: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #3                  // class generic/SimpleHolder
       3: dup
       4: invokespecial #4                  // Method "<init>":()V
       7: astore_1
       8: aload_1
       9: ldc           #5                  // String Item
      11: invokevirtual #6                  // Method setObj:(Ljava/lang/Object;)V
      14: aload_1
      15: invokevirtual #7                  // Method getObj:()Ljava/lang/Object;
      18: checkcast     #8                  // class java/lang/String
      21: astore_2
      22: return
  1. setObj() 和 getObj() 方法將直接存儲和產生值,而轉型是在調用 getObj() 的時候接受檢查的。
  2. 現在將泛型合併到上面的代碼中:
public class SimpleHolder <T>{
    private T obj;

    public T getObj() {
        return obj;
    }

    public void setObj(T obj) {
        this.obj = obj;
    }

    public static void main(String[] args) {
        SimpleHolder<String> holder = new SimpleHolder<String>();
        holder.setObj("Item");
        String obj = holder.getObj();
    }
}

//從 getObj() 返回之後的轉型消失了, 但是我們還知道傳遞給setObj() 的值在編譯器會接受檢查,下面是相關字節碼。

 public T getObj();
    Code:
       0: aload_0
       1: getfield      #2                  // Field obj:Ljava/lang/Object;
       4: areturn

  public void setObj(T);
    Code:
       0: aload_0
       1: aload_1
       2: putfield      #2                  // Field obj:Ljava/lang/Object;
       5: return
public static void main(java.lang.String[]);
    Code:
       0: new           #3                  // class generic/GenericHolder
       3: dup
       4: invokespecial #4                  // Method "<init>":()V
       7: astore_1
       8: aload_1
       9: ldc           #5                  // String item
      11: invokevirtual #6                  // Method setObj:(Ljava/lang/Object;)V
      14: aload_1
      15: invokevirtual #7                  // Method getObj:()Ljava/lang/Object;
      18: checkcast     #8                  // class java/lang/String
      21: astore_2
      22: return
  1. 所產生的的字節碼是相同的。對進入setObj() 的類型檢查是不需要的,因爲這將由編譯器執行。
  2. 而對從 getObj() 返回的值進行轉型仍舊是需要的,但這與你自己必須執行的操作是一樣的___此處它將由編譯器自動插入,因此你寫入和讀取的代碼的噪聲將更小。
  3. 由於所產生的 getObj() 和 setObj() 的字節碼相同,所以在泛型中的所有動作都發生在邊界處___對傳遞進來的值進行額外的編譯期檢查,並插入對傳遞出去值的轉型。這有助於澄清對擦除的混淆,記住: 邊界就是發生動作的地方

  

擦除的補償

  • 正如我們看到的,擦除丟失了在泛型代碼中執行某些操作的能力。任何在運行時需要知道確切類型信息的操作都將無法工作。
public class Erased<T> {
    
    private final int SIZE=100;
    
    static void f(Object obj){
        //下面這段代碼是錯誤的 
        if (obj instanceof  T){  
            T t=new T();
            T [] array=new T[SIZE];
            T[] arrays=(T)Object[SIZE]; //未經檢查的警告
        }
    }
}
  1. 偶爾可以繞過這些問題來編程,但是有時必須通過引入類型標籤來對擦除進行補償。這意味着你需要顯式地傳遞你的類型的 Class 對象,以便你可以在類型的表達式中使用它。
  2. 例如,在前面實例中對使用 instanceof 的嘗試最終失敗了,因爲其類型信息已經被擦除了。如果引入類型標籤,就可以轉而使用動態的 isInstance();
class Building{}

class House extends Building{}

public class Erased<T> {
    Class<T> tClass;
    public Erased(Class<T> tClass) {
        this.tClass = tClass;
    }
    boolean f(Object obj){
        return tClass.isInstance(obj);
    }
    public static void main(String[] args) {
        //1, 創建泛型 爲 Building類
        Erased<Building> erased=new Erased<>(Building.class);
        System.out.println(erased.f(new House()));
        System.out.println(erased.f(new Building()));

        //2,創建泛型爲 House類
        Erased<House> erased1=new Erased<>(House.class);
        System.out.println(erased1.f(new Building()));
        System.out.println(erased1.f(new House()));
    }
}

//運行結果爲

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

 

創建類型實例

  • 在 Erased.java(編譯錯誤的版本中) 中對創建一個 new T() 的嘗試講將無法實現,部分原因是因爲擦除,而另一部分原因就是因爲編譯器不能驗證T具有默認(無參)構造器。
  • Java中的解決方案是傳遞一個工廠對象,並用它來創建新的實例。最便利的工廠對象就是Class 對象,因此如果使用類型標籤,那麼你就可以使用 newInstance() 來創建類型的新對象。
public class ClassAsFactory<T> {
    T t;
    public ClassAsFactory(Class<T> tClass) {
        try {
            t = tClass.newInstance();
        } catch (Exception e) {
           throw new RuntimeException(e);
        }
    }
}

class Employee{}

class InstantiateGenericType{
    public static void main(String[] args) {
        ClassAsFactory<Employee> employeeClassAsFactory=new ClassAsFactory<>(Employee.class);
        System.out.println("ClassAsFactory<Employee> succeeded");

        try {
            ClassAsFactory<Integer> classAsFactory=new ClassAsFactory<>(Integer.class);
        }catch (Exception e){
            System.out.println(" ClassAsFactory<Integer> failed");
        }
    }
}

//運行結果爲
ClassAsFactory<Employee> succeeded
ClassAsFactory<Integer> failed
  1. 這可以編譯,但是會因 ClassAsFactory<Integer> 而失敗,因爲Integer 沒有任何默認的構造器。因爲這個錯誤不是在編譯期間捕獲的,所以Sun 的夥計們對這種方式並不贊成,他們建議使用顯式的工廠,並將限制其類型,使得只能接受實現了這個工廠的類。
interface Factory<T>{
    T create();
}

//創建對象工廠
class Foo2<T>{
    private T t;
    public <F extends Factory<T>> Foo2(F factory){
        t=factory.create();
    }
}

class IntegerFactory implements Factory<Integer>{

    @Override
    public Integer create() {
        return new Integer(0);
    }
}

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


public class FactoryConstraint {
    public static void main(String[] args) {
        new Foo2<>(new IntegerFactory());
        new Foo2<>(new Widget.TestFactory());
    }
}
  1. 注意,這確實只是傳遞 Class<T> 的一種變體。兩種方式都傳遞了工廠對象,Class<T> 碰巧是內建的工廠對象,而上面的方式創建了一個顯式的工廠對象,但是你卻獲得了編譯期檢查。
  2. 另一種方式是模板方法設計模式。在下面的示例中, get() 是模板方法,而create() 在子類中定義的,用來生成子類類型的對象。
public abstract class GenericWithCreate <T>{
    final T element;

     GenericWithCreate() {
        this.element = create();
    }

    abstract T create();
}

class X{}

class Creator extends GenericWithCreate<X>{

    @Override
    X create() {
        return new X();
    }

    void f(){
        System.out.println(element.getClass().getSimpleName());
    }
}

class CreatorGeneric{
    public static void main(String[] args) {
        Creator creator = new Creator();
         creator.f();
    }
}

//運行結果爲
X

 

 

泛型數組

  • 正如你在 Erased.java中所見(Erased 類中出現錯誤的版本),不能創建泛型數組。一般的解決方案是在任何要創建泛型數組的地方都使用 ArrayList
public class ListOfGenerics<T> {

    private List<T> lists=new ArrayList<>();
    
    void add(T t){
        lists.add(t);
    }
    
    T get(int index){
        return lists.get(index);
    }
}
  1. 這裏你將獲取數組的行爲,以及由泛型提供的編譯器的類型安全。
  2. 有時,你仍舊希望創建泛型類型的數組(例如,ArrayList 內部使用的是數組)。有趣的是,可以按照編譯器喜歡的方式來定義一個引用,例如
class Generic<T>{}

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

    public static void main(String[] args) {
        //compiles produces classCastException
        //編譯產生 classCastException
        //gia=(Generic<Integer>[])new Object[SIZE];

        //運行時類型是原始(擦除)類型
        gia = (Generic<Integer>[]) new Generic[SIZE];
        System.out.println(gia.getClass().getSimpleName());

        gia[0] = new Generic<Integer>();

        
        //編譯時錯誤
        // gia[1]=new Object();

        //在編譯時發現類型不匹配
        //gia[2]=new Generic<Double>();

    }
}

//運行結果爲

Generic[]
  1. 問題在意數組將跟蹤它們的實際類型,而這個類型是在數組被創建時確定的,因此,即使 gia 已經被轉型爲 Generic<Integer>[] ,而這個信息只存於編譯期。
  2. 在運行時,它仍舊是Object數組,而這將引發問題。成功創建泛型數組的唯一方式就是創建一個被擦除類型的新數組,然後對其轉型。
public class GenericArray<T> {

    private T [] array;

    public GenericArray(int size) {
        array= (T[]) new Object[size];
    }

    public void put(int index,T item){
        array[index] = item;
    }

    public T get(int index){
        return array[index];
    }

    public T[] rep(){
        return array;
    }

    public static void main(String[] args) {
        GenericArray<Integer> genericArray =new GenericArray<>(5);
        //ClassCastException
        //Integer [] integers=genericArray.rep();

        //下面這個是沒問題的
        java.lang.Object [] objects=genericArray.rep();
    }
}
  1. 與前面相同,我們並不能聲明 T[] array=new T[size],因此我們創建了一個對象數組,然後對其轉型。
  2. rep() 方法將返回 T[] ,它在 main() 中將用於  genericArray ,因此如果調用它,並嘗試着將結果作爲 Integer [] 引用來捕獲,就會得到 ClassCastException ,這還是因爲實際的運行時類型是 Object[]。
  3. 因爲有了擦除,數組在運行時類型就只能是 Object[] ,如果我們立即將其轉型爲 T[] ,那麼在編譯期該數組的實際類型就將丟失,而編譯器可能會錯過某些潛在的錯誤檢查。
  4. 正因爲這樣,最好是在集合內部使用 Object[] ,然後當你使用數組元素時,添加一個對T 的轉型。讓我們看看這是如何運作的如下。
public class GenericArray2<T> {

    private Object[] array;

    public GenericArray2(int size) {
        array=new Object[size];
    }

    public void put(int index,T item){
        array[index] =item;
    }

    public T get(int index){
        return (T) array[index];
    }

    public T[] rep(){
        return (T[]) array;
    }

    public static void main(String[] args) {
        GenericArray2<Integer> integerGenericArray2=new GenericArray2<>(5);

        for (int i = 0; i < 5; i++) {
            integerGenericArray2.put(i,i);
        }

        for (int i = 0; i < 5; i++) {
            Integer integer = integerGenericArray2.get(i);
            System.out.println(integer);
        }

        try {
            Integer[] integers = integerGenericArray2.rep();
        }catch (Exception e){
            System.out.println("異常了");
        }
    }
}

//運行結果爲

0
1
2
3
4
異常了
  1. 初看起來,這好像沒有多大變化,只是轉型挪了地方。但是現在的內部是Object[] 而不是 T[] ,當get() 方法被調用時,它將對象轉型爲 T ,這實際上是正確的類型,因此這是安全的。
  2. 然而,如果你調用 rep() ,它還是嘗試着將 Object[] 轉型爲 T[] ,這仍舊是不正確的,將在編譯器產生警告,在運行時產生異常。因此,沒有任何方式可以推翻底層的數據類型,它只能是 Object[] 。在內部將 array 當做Object[] 而不是T [] 處理的優勢是: 我們不太可能忘記這個數組的運行時類型,從而以外地引入缺陷(儘管大多數也可能是所有這類缺陷都可以在運行時快速地探測到)
  3. 對於新代碼,應該傳遞一個類型標記。在這種情況下, GenericArray 看起來會像下面這樣。
public class GenericArrayWithTypeToken<T> {

    private T[] array;

    public GenericArrayWithTypeToken(Class<T> tClass,int size){
        array= (T[]) Array.newInstance(tClass,size);
    }

    public void put(int index,T item){
        array[index]=item;
    }
    public T get(int index){
        return array[index];
    }
    public T[] rep(){
        return array;
    }

    public static void main(String[] args) {
        GenericArrayWithTypeToken<Integer> integers=new GenericArrayWithTypeToken<>(Integer.class,5);
        Integer[] rep = integers.rep();
        //這個是沒有錯誤的
    }
}
  1. 類型Class<T> 被傳遞到構造器中,以便從擦除中恢復,使得我們可以創建需要的實際類型的數組。
  2. 一旦我們獲得了實際類型,就可以返回它,並獲得想要的結果,就像在 main() 中看到的那樣。該數組的運行時類型是確切類型T []。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章