Java 1.5 特性 : Generic Types

C++ 中的泛型

先前我撰寫 C++ 時,還沒有泛型(Generic types)的字眼出現,最多是使用 template 來達成對不同型別的支援。像是:

template <class Type>
class Stack {
    Type st[MAX];
    int t;

    public:
        void push(Type v);
        // other code
};

Stack<char> s;  // declare

不過在 C++ STL(Standard Template Library)出現,各個 C++ 編譯器相繼支援後,泛型一詞才在資訊界中廣為流傳。STL 提供了字串、容器、演算法等眾多功能,使得 C++ 增色不少,其中泛型是相當重要的概念。無論是各種類別,都可以放在容器之中,並且使用演算法函式庫來處理。例如:

vector<string> *dirs = new vector<string>;  // data list
addSomeData(dirs);
sort(dirs->begin(), dirs->end());  // sort list

typedef vector<string>::iterator iter;
typedef string::size_type size_type;
for (iter it = dirs->begin(); it != dirs->end(); it++) {
    size_type dpos = (*it).find_last_of("."); // find .
    string fileName = (*it).replace(dpos + 1, 3, "txt"); //replace
    cout<<"Read "<<(*it)<<" and create "<<fileName;
}

C++ 有泛型的出現,很大一個原因是為了要處理各種不同的型別,包括了 STL 中新增和使用者自行定義的部份。和 C++ 不同的是,Java 一開始並沒有這樣的設計,因為 Java 所有的物件都有共同的祖先:java.lang.Object,所以 Java 中的函式若要處理多型別時,只需適當地定義參數和 / 或傳回值為 Object 即可,這正是多型的徹底應用之一。反之 C++ 中的類別沒有這樣的關係,因此泛型在 C++ 中就顯得特別重要。

不過在這邊有些缺點。因為 C++ 的泛型是使用 template 來達成的,而 template 會在遇到不同型別時產生相對應的副本。例如 Type add(Type t, Type s) 這項函式定義會在 add(4, 1) 時出現 int add(int, int) 的程式碼,並在 add(1.1, 2.2) 時出現 float add(float, float) 的程式碼,也就是整個整式因為這個關係,而變得愈來愈肥大。

Java 中的泛型

Java 1.5 中已經支援泛型了。目前主要在 java.util 中的類別有比較多的例子,另外還有在 java.lang.Comparable, java.lang.Class, java.lang.Enum 及其相關類別。底下是用 ListVector 來測試泛型:

import java.util.*;
public class GenericTest {
    public static void main(String[] args) {
        int size = 6;
        List<Integer> list =  new Vector<Integer>();

        for (int i = 0; i < size; i++) {
            list.add(new Integer(size - i));
        }
        for (int i = 0; i < size; i++) {
            System.out.print(list.get(i).intValue() + "-");
        }
    }
}

執行結果:

6-5-4-3-2-1-

泛型在此所帶來的好處是只要在宣告時我們指定好類別,那麼接下來後面的程式中它會自動幫我們轉型,並且也會檢查後續加入容器中的物件是否可轉型成我們指定的類別。上例中,如果增加一行 list.add("test"),那麼在編譯時就會出現錯誤,會說 cannot find symbol : method add(java.lang.String)

這使得原本放在執行階段才會檢查出來的問題,提昇到了編譯時期。除了降低 bug 出現的機會,也讓程式設計師在除錯上方便許多。另外 Java 使用動態連結技術,並且有共同的 Object 祖先做為最根本的多型,因此並不需要像 C++ 一樣,為每一個型別產生不同的程式碼副本。

泛型中使用多型

List 中的 Integer 其實可以看成是欲放入容器中物件的類別,因此接下來的 add()get() 都會將其傳入值和傳回值轉型成其類別。所以我們也可以將多型運用在這上面,例如:

mport java.util.*;
public class GenericTest2 {
    public static void main(String[] args) {
        List<GenericSuper> list =  new Vector<GenericSuper>();
        // 改成 List<GenericSub> list = Vector<GenericSuper> 
        // 或 List<GenericSuper> list = Vector<GenericSub> 的話,編譯會出現 incompatible type
 
        for (int i = 0; i < 4; i++) {
            list.add(i % 2 == 0 ? new GenericSuper() : new GenericSub());
        }

        Iterator<GenericSuper> iterator = list.iterator();
        for (int i = 0; iterator.hasNext(); i++) {
            GenericSuper g = iterator.next();
            System.out.println(i + ":" + g.getName());
        }
    }
}

class GenericSuper {
    public String getName() {
        return "Super";
    }
}
class GenericSub extends GenericSuper {
    public String getName() {
        return "Sub";
    }
}

執行結果:

0:Super
1:Sub
2:Super
3:Sub

使用舊類別和舊函式

使用 1.5 的語法時,在編譯的時候要指定參數才能順利編譯。不過在 1.5 中因為容器類別都加上泛型的支援,使得早期我們慣用的語法在此情況下編譯的話,會丟出 warning 訊息。例如:

List<GenericSuper> list =  new Vector<GenericSuper>();
List list2 = new Vector();
for (int i = 0; i < 4; i++) {
    list.add(i % 2 == 0 ? new GenericSuper() : new GenericSub());
    list2.add(new GenericSuper());
}

會導致下列 warning (需加上 -Xlint:unchecked 參數。注意,雖然有 warning,但仍會產生 class 檔案,並且仍可正常執行):

GenericTest2.java:10: warning: [unchecked] unchecked call to add(E) 
as a member of the raw type java.util.List
      list2.add(new GenericSuper());
           ^
1 warning

自定泛型類別

除了使用 java.util 套件中的泛型機制外,我們也可以自行定義泛型類別:

import java.util.*;
public class GenericTest3 {
    public static void main(String[] args) {
        GenericType<String> gt = new GenericType<String>();
        for (int i = 0; i < 4; i++) {
            gt.add("number" + i);
            System.out.println(gt.get(i));
        }
    }
}

class GenericType<T> {
    private T test;
    private List<T> list;
    public GenericType() {
        list = new Vector<T>();
    }
    public void add(T t) {
        list.add(t);
    }
    public T get(int i) {
        return list.get(i);
    }
}

上面的例子我們定義了一個 GenericType 的類別。當然,你也可以撰寫一個新的類別來繼承 GenericType,使得其子類別也有泛型的功能。總之,OOP 中原有的多型,和新增的泛型,你可以依照需求來做各種不同的組合。我猜想在這上面日後應該會有不少 pattern 或觀念出來,不過目前 Java 1.5 尚未正式版,所以日後來再觀察吧。

更複雜的泛型

除了在 List 部份外,SetMap 系列當然也有泛型,下面測試 Map 部份:

import java.util.*;
public class GenericTest4 {
    public static void main(String[] args) {
        Map<Integer, String> map = new HashMap<Integer, String>();
        for (int i = 0; i < 4; i++) {
            map.put(new Integer(i), "number" + i);
        }
        System.out.println(map.get(new Integer(2)));
    }
}

class GenericType2<T, S> {
    public void add(T t) {
        // do nothing
    }
}

從上例可以看到,我們可以一次使用兩種不用的泛型,也就是 key 為 Integer,value 為 String。在編譯時期,javac 會自動檢查傳入和輸出的型別,並試著去做轉型。如果有問題,就利出 imcompatible type 訊息。這邊我們也可以看到,我們自行定義的類別也可以使用兩種不同的泛型行為,甚至可以使用底下的類別定義:

class GenericType3<A, B, C> implements Comparable<GenericType3> {
    private GenericType2<A, B> a;
    private GenericType2<Map<Integer, String>, List<GenericType2<A, B>>> b;
    public int compareTo(GenericType3 g) {
        return 0;
    }
}
class GenericType4<A>{
    public GenericType3<Comparable<A>, A, A> max(List<Comparable<A>> list) {
        return null;
    }
}

可以看到上例中 GenericType3 最後一個欄位 b 以及 GenericType4max 方法可以裝下多麼複雜的泛型定義。

Leave a Comment

Your email address will not be published. Required fields are marked *

two + fourteen =

Scroll to Top