wiki:Generic

Version 9 (modified by waue, 16 years ago) (diff)

--

泛型

當您定義類別時,發現到好幾個類別的邏輯其實都相同,就只是當中所涉及的型態不一樣時,使用複製、貼上、取代的功能來撰寫程式只是讓您增加不必要的檔案管理困擾。

由於Java中所有定義的類別,都以Object為最上層的父類別,所以在 J2SE 5.0 之前,Java程式設計人員可以使用Object來解決上面這樣的需求,為了讓定義出來的類別可以更加通用(Generic),傳入的值或傳回的物件都是以Object為主,當您要取出這些物件來使用時,必須記得將介面轉換為原來的類型,這樣才可以操作物件上的方法。

然而使用Object來撰寫泛型類別(Generic Class)留下了一個問題,因為您必須要轉換介面,粗心的程式設計人員往往會忘了要作這個動作,或者是轉換介面時用錯了型態(像是該用Boolean卻用了Integer),要命的是,語法上是可以的,所以編譯器檢查不出錯誤,真正的錯誤要在執行時期才會發生,這時惱人的!ClassCastException就會出來搞怪,在使用Object設計泛型程式時,程式人員要再細心一些、小心一些。

在J2SE 5.0之後,提出了針對泛型(Generics)設計的解決方案,要定義一個簡單的泛型類別是簡單的,直接來看個例子:

  • GenericFoo.java
    public class GenericFoo<T> {
        private T foo;
     
        public void setFoo(T foo) {
            this.foo = foo;
        }
     
        public T getFoo() {
            return foo;
        }
    }
    

<T> 用來宣告一個型態持有者(Holder)T,之後您可以用 T 作為型態代表來宣告變數(參考)名稱,然後您可以像下面的程式來使用這個類別:

GenericFoo<Boolean> foo1 = new GenericFoo<Boolean>();
GenericFoo<Integer> foo2 = new GenericFoo<Integer>();
 
foo1.setFoo(new Boolean(true));
Boolean b = foo1.getFoo();
 
foo2.setFoo(new Integer(10));
Integer i = foo2.getFoo();

回過頭來看看下面的宣告:

GenericFoo<Boolean> foo1 = new GenericFoo<Boolean>();
GenericFoo<Integer> foo2 = new GenericFoo<Integer>();

GenericFoo< Boolean>宣告的foo1與GenericFoo< Integer>宣告的foo2是相同的類型嗎?答案是否定的,基本上它們分屬於兩個不同類別的類型,即「相當於」下面兩個類型(只是個比喻):

public class GenericFooBoolean {
    private Boolean foo;
 
    public void setFoo(Boolean foo) {
        this.foo = foo;
    }
 
    public Boolean getFoo() {
        return foo;
    }
}

以及:

public class GenericFooInteger {
    private Integer foo;
 
    public void setFoo(Integer foo) {
        this.foo = foo;
    }
 
    public Integer getFoo() {
        return foo;
    }
}

所以您不可以將 foo1 指定給 foo2,或是將 foo2 指定給 foo1,編譯器會回報錯誤。

幾個定義泛型的例子

您可以在定義泛型類別時,宣告多個類型持有者,例如:

  • GenericFoo.java
public class GenericFoo<T1, T2> {
    private T1 foo1;
    private T2 foo2;
 
    public void setFoo1(T1 foo1) {
        this.foo1 = foo1;
    }
 
    public T1 getFoo1() {
        return foo1;
    }
 
    public void setFoo2(T2 foo2) {
        this.foo2 = foo2;
    }
 
    public T2 getFoo2() {
        return foo2;
    }
} 

您可以如下使用!GenericFoo類別,分別以Integer與Boolean取代T1與T2:

GenericFoo<Integer, Boolean> foo = new GenericFoo<Integer, Boolean>();

如果是陣列的話,可以像這樣:

  • GenericFoo.java
public class GenericFoo<T> {
    private T[] fooArray;

    public void setFooArray(T[] fooArray) {
        this.fooArray = fooArray;
    }

    public T[] getFooArray() {
        return fooArray;
    }
} 

您可以像下面的方式來使用它:

String[] strs = {"caterpillar", "momor", "bush"};
 
GenericFoo<String> foo = new GenericFoo<String>();
foo.setFooArray(strs);
strs = foo.getFooArray();

來改寫一下 Object 類別 中的 SimpleCollection:

  • SimpleCollection.java
public class SimpleCollection<T> { 
    private T[] objArr; 
    private int index = 0; 
 
    public SimpleCollection() { 
        objArr = (T[]) new Object[10]; // 預設10個物件空間 
    } 
 
    public SimpleCollection(int capacity) { 
        objArr = (T[]) new Object[capacity]; 
    } 
 
    public void add(T t) { 
        objArr[index] = t; 
        index++; 
    } 
 
    public int getLength() { 
        return index; 
    } 
 
    public T get(int i) { 
        return (T) objArr[i]; 
    } 
} 

現在您可以直接使用它來當作特定類型物件的容器,例如:

  • Test.java
    public class Test { 
        public static void main(String[] args) { 
            SimpleCollection<Integer> c = 
                    new SimpleCollection<Integer>(); 
     
            for(int i = 0; i < 10; i++) {
                c.add(new Integer(i));
            }
    
            for(int i = 0; i < 10; i++) {
                Integer k = c.get(i);
            }
        } 
    } 
    

另一個!SimpleCollection的寫法也可以如下,作用是一樣的:

  • SimpleCollection.java
    public class SimpleCollection<T> { 
        private Object[] objArr; 
        private int index = 0; 
     
        public SimpleCollection() { 
            objArr = new Object[10]; // 預設10個物件空間 
        } 
     
        public SimpleCollection(int capacity) { 
            objArr = new Object[capacity]; 
        } 
     
        public void add(T t) { 
            objArr[index] = t; 
            index++; 
        } 
     
        public int getLength() { 
            return index; 
        } 
     
        public T get(int i) { 
            return (T) objArr[i]; 
        } 
    } 
    

如果您已經定義了一個泛型類別,想要用這個類別來於另一個泛型類別中宣告成員的話要如何作?舉個實例,假設您已經定義了下面的類別:

  • GenericFoo.java
    public class GenericFoo<T> {
        private T foo;
     
        public void setFoo(T foo) {
            this.foo = foo;
        }
     
        public T getFoo() {
            return foo;
        }
    } 
    

您想要寫一個包裝類別(Wrapper),這個類別必須也具有!GenericFoo的泛型功能,您可以這麼寫:

  • WrapperFoo.java
    public class WrapperFoo<T> {
        private GenericFoo<T> foo;
        
        public void setFoo(GenericFoo<T> foo) {
            this.foo = foo;
        }
     
        public GenericFoo<T> getFoo() {
            return foo;
        }
    } 
    

這麼一來,您就可以保留型態持有者 T 的功能,一個使用的例子如下:

GenericFoo<Integer> foo = new GenericFoo<Integer>();
foo.setFoo(new Integer(10));
 
WrapperFoo<Integer> wrapper = new WrapperFoo<Integer>();
wrapper.setFoo(foo);

限制泛型可用類型

在定義泛型類別時,預設您可以使用任何的型態來實例化泛型類別中的型態持有者,但假設您想要限制使用泛型類別時,只能用某個特定型態或其子類別才能實例化型態持有者的話呢?

您可以在定義型態持有者時,一併使用"extends"指定這個型態持有者必須是擴充某個類型,舉個實例來說:

  • ListGenericFoo.java
import java.util.List;

public class ListGenericFoo<T extends List> {
    private T[] fooArray;

    public void setFooArray(T[] fooArray) {
        this.fooArray = fooArray;
    }

    public T[] getFooArray() {
        return fooArray;
    }
} 

!ListGenericFoo在宣告類型持有者時,一併指定這個持有者必須擴充自List介面(interface),在限定持有者時,無論是要限定的對象是介面或類別,都是使用"extends"關鍵字。

您使用"extends"限定型態持有者必須是實作List的類別或其子類別,例如!LinkedList與ArrayList,下面的程式是合法的:

ListGenericFoo<LinkedList> foo1 =
                  new ListGenericFoo<LinkedList>();
ListGenericFoo<ArrayList> foo2 =
                  new ListGenericFoo<ArrayList>();

但是如果不是List的類別或是其子類別,就會發生編譯錯誤,例如下面的程式通不過編譯:

ListGenericFoo<!HashMap> foo3 = new ListGenericFoo<!HashMap>();

編譯器會回報以下錯誤訊息:
type parameter java.util.HashMap is not within its bound
ListGenericFoo<HashMap> foo3 = new ListGenericFoo<HashMap>();

!HashMap並沒有實作List介面,所以無法用來實例化型態持有者,事實上,當您沒有使用extends關鍵字限定型態持有者時,預設則是Object下的所有子類別都可以實例化型態持有者,即只寫<T>時就相當於<T extends Object>。

型態通配字元

假設您撰寫了一個泛型類別:

  • GenericFoo.java
    public class GenericFoo<T> {
        private T foo;
     
        public void setFoo(T foo) {
            this.foo = foo;
        }
     
        public T getFoo() {
            return foo;
        }
    }
    

分別使用下面的程式宣告了foo1與foo2兩個參考名稱:

GenericFoo<Integer> foo1 = null;
GenericFoo<Boolean> foo2 = null;

那麼 foo1 就只接受GenericFoo<Integer>的實例,而foo2只接受GenericFoo<Boolean>的實例。

現在您有這麼一個需求,您希望有一個參考名稱foo可以接受所有下面的實例(List、Map或List介面以及其實介面的相關類別,在J2SE 5.0中已經針對泛型功能作了改寫,在這邊仍請將之當作介面就好,這是為了簡化說明的考量):

foo = new GenericFoo<ArrayList>();
foo = new GenericFoo<LinkedList>();

簡單的說,實例化型態持有者時,它必須是實作List的類別或其子類別,要宣告這麼一個參考名稱,您可以使用 '?' 通配字元,並使用"extends"關鍵字限定型態持有者的型態,例如

GenericFoo<? extends List> foo = null;
foo = new !GenericFoo<ArrayList>();
.....
foo = new GenericFoo<LinkedList>();
...

如果指定了不是實作List的類別或其子類別,則編譯器會回報錯誤,例如:

GenericFoo<? extends List> foo = new GenericFoo<HashMap>();

上面這段程式編譯器會回報以下的錯誤:

incompatible types
found : GenericFoo<java.util.HashMap>
required: GenericFoo<? extends java.util.List>
GenericFoo<? extends List> foo = new GenericFoo<HashMap>();

這樣的限定是很有用的,例如如果您想要自訂一個showFoo()方法,方法的內容實作是針對List而制定的,例如:

public void showFoo(GenericFoo foo) {

針對List而制定的內容

}

您當然不希望任何的型態都可以傳入showFoo()方法中,您可以使用以下的方式來限定,例如:
public void showFoo(GenericFoo<? extends List> foo) {}

這麼一來,如果有粗心的程式設計人員傳入了您不想要的型態,例如GenericFoo<Boolean>型態的實例,則編譯器都會告訴它這是不可行的,在宣告名稱時如果指定了<?>而不使用"extends",則預設是允許Object及其下的子類,也就是所有的Java物件了,那為什麼不直接使用!GenericFoo宣告就好了,何必要用GenericFoo<?>來宣告?使用通配字元有點要注意的是,透過使用通配字元宣告的名稱所參考的物件,您沒辦法再對它加入新的資訊,您只能取得它的資訊或是移除它的資訊,例如:

GenericFoo<String> foo = new GenericFoo<String>();
foo.setFoo("caterpillar");
GenericFoo<?> immutableFoo = foo;

// 可以取得資訊
System.out.println(immutableFoo.getFoo());

// 可透過immutableFoo來移去foo所參考實例內的資訊
immutableFoo.setFoo(null);

// 不可透過immutableFoo來設定新的資訊給foo所參考的實例
// 所以下面這行無法通過編譯
//  immutableFoo.setFoo("良葛格");

所以使用<?>或是<? extends SomeClass>的宣告方式,意味著您只能透過該名稱來取得所參考實例的資訊,或者是移除某些資訊,但不能增加它的資訊,因為只知道當中放置的是!SomeClass的子類,但不確定是什麼類的實例,編譯器不讓您加入物件,理由是,如果可以加入物件的話,那麼您就得記得取回的物件實例是什麼形態,然後轉換為原來的型態方可進行操作,這就失去了使用泛型的意義。

事實上,GenericFoo<?> immutableFoo相當於GenericFoo immutableFoo。

除了可以向下限制,您也可以向上限制,只要使用"super"關鍵字,例如:

GenericFoo<? super StringBuilder> foo;

如此,foo就只接受 StringBuilder 及其上層的父類型態之物件。

擴充泛型類別、實作泛型介面

您可以擴充一個泛型類別,保留其型態持有者,並新增自己的型態持有者,例如先寫一個父類別:

  • GenericFoo.java
    public class GenericFoo<T1, T2> {
        private T1 foo1;
        private T2 foo2;
     
        public void setFoo1(T1 foo1) {
            this.foo1 = foo1;
        }
     
        public T1 getFoo1() {
            return foo1;
        }
     
        public void setFoo2(T2 foo2) {
            this.foo2 = foo2;
        }
     
        public T2 getFoo2() {
            return foo2;
        }
    } 
    

再來寫一個子類別擴充上面的父類別:

    * SubGenericFoo.java

public class SubGenericFoo<T1, T2, T3> 
                               extends GenericFoo<T1, T2> {
    private T3 foo3;
  
    public void setFoo3(T3 foo3) {
        this.foo3 = foo3;
    }
 
    public T3 getFoo3() {
        return foo3;
    }
} 

如果決定要保留型態持有者,則父類別上宣告的型態持有者數目必須齊全,也就是說上式中,T1與T2都要出現,如果不保留型態持有者,則繼承下來的T1與 T2自動變為Object,建議當然是父類別的型態持有者都保留。

介面實作也是類似,例如先定義一個介面:

  • IFoo.java
    public interface IFoo<T1, T2> {
        public void setFoo1(T1 foo1);
        public void setFoo2(T2 foo2);
        public T1 getFoo1();
        public T2 getFoo2();
    }
    

實作時如下,保留所有的型態持有者:

  • GenericFoo.java
    public class GenericFoo<T1, T2> implements IFoo<T1, T2> {
        private T1 foo1;
        private T2 foo2;
     
        public void setFoo1(T1 foo1) {
            this.foo1 = foo1;
        }
     
        public T1 getFoo1() {
            return foo1;
        }
     
        public void setFoo2(T2 foo2) {
            this.foo2 = foo2;
        }
     
        public T2 getFoo2() {
            return foo2;
        }
    }
    

引自java Gossip