參數(Parametric)多型允許函式設計時不理會參數實際型態,函式實作版本只有一個,呼叫時則可套用不同類型的引數,由於程式語言上的多型,是可使用一致介面來處理不同資料型態,從這點來看,它才是多型最純粹的型式。

具靜態型別安全檢查功能的泛型函式
有些需求使用函式解決時,並不需要在意傳入的資料型態,設計函式時,可以空泛地定義參數型態,這類函式稱為泛型(Generic)函式。

例如交換陣列兩元素的需求,若使用JDK 1.5以後的版本,可以使用泛型語法設計為 T[] swap(T[] arrs, int i, int j) { T orgi = arrs[i]; arrs[i] = arrs[j]; arrs[j] = orgi; return arrs; },傳入陣列的元素型態都是T型態,但T實際上是型態變數(Type variables),用以取代實際的型態宣告,而泛型語法就是Java中實現參數多型的方式。

參數多型讓函式定義時更具涵蓋性,並具有靜態型別編譯時期安全檢查功能。例如可使用String[] result = swap(original, 1, 2)來呼叫swap函式,傳入物件是String[],傳回物件就會是String[],若使用其他型態宣告result,編譯器就會檢查出這個錯誤。

JDK 1.4前沒有泛型語法,相同需求必須使用次型態多型來達成,也就是定義函式為Object[] swap(Object[] arrs, int i, int j),如果實際上傳入物件是String[],呼叫後傳回物件必須進行轉型,例如String[] result = (String[]) swap(original, 1, 2),轉型語法只是要求編譯器停止該語句的型態檢查,真正的轉型是在執行時期進行,既然編譯時期使用轉型語法要求編譯器不要檢查型態,若執行時期轉型失敗,就得自行負責。

參數多型實際須依賴類型推斷(Type inference),如String[] result = swap(original, 1, 2)時,編譯器可由original推斷出T是String,從而得知傳回物件為String[]。有時為了規範型態變數範圍,可加上約束。例如取陣列中最大值的max函式,可定義為 T max(T[] arrs):

T max = arrs[0];
for(T elem : arrs) if(elem.compareTo(max) > 0) { max = elem; }
return max;

這是由於Java中定義物件可比較性時,必須實現Comparable行為,要求編譯器進一步約束傳入物件須具有Comparable行為,也可以說為了讓編譯器確認每個元素都有compareTo()方法供操作,讓max函式現階段完成編譯。

類別型態也能參數化加深了複雜性

在函數式語言中,隨處可見參數多型的應用,因而都直接稱「多型」而不特別稱參數多型。以Haskell為例,配合編譯器強大的類型推斷功能,定義函式時幾乎可忽略型態變數的存在(因為基本上連型態都不用宣告)。例如同樣以取清單中最大值的max'函式為例,Haskell中可以如下定義:
max' [] = error "empty list"
max' [x] = x
max' (x:xs) = max x (max' xs)

在上例中看不到型態變數的存在,也看不到型態類(Typeclasses)約束,但實際上max'函式宣告是(Ord a) => [a] -> a,Ord即類似Java中Comparable的角色,然而,這個宣告定義可由編譯器自動推斷整個函式定義得知,強大的型態推斷結合參數多型,使得開發者減輕不少型態定義上的負擔。

參數多型的原意,是減輕開發者使用靜態語言時,必須時刻在意型態問題的負擔。對於將執行時期發生轉型錯誤的可能性,移轉至編譯時期就檢查出錯誤而言,Java的泛型確實達到這個目的,然而Java泛型語法本身可讀性不佳,單就前幾個Java泛型函式定義範例,就可看出端倪,Java本身又支援物件導向,如果將參數多型從函式擴展到類別,讓類別型態也可以參數化,就會使得情況更加複雜。

以Java的群集(Collection)為例,由於收集的物件多半是同質的(Homogeneous),為了讓編譯器協助開發者檢查出型態錯誤,而引進了Colllection的泛型語法,E是型態變數,如果開發者宣告變數時使用Collection,那麼編譯器就不允許收集String以外的物件,從而避免了執行時期ClassCastException的問題。

然而實際上,類別型態也可以參數化,等於是對原有型態系統進了極大擴充。Collection可看作一種型態、Collection也是一種型態,既然將型態變數實例化後的結果都可視作一種型態,那麼就會衍生出Collection>這類複雜的語法,即使Java群集框架的實現領導者Joshua Bloch喜歡泛型,認為在某些方面還是能實現簡潔度,但看到Enum>之類的語法,就覺得泛型的設計還不夠成熟到能放入Java中。

繼承與參數多型又衍生出變異性的問題
類別型態參數化,加深了原有型態系統的複雜度,若再加上繼承,得再考慮更複雜的變異性(Variance)。在型態系統中,如果型態階層與取代階層的方向一致,稱為正變性(Covariance),反之則稱為逆變性(Contravariance),兩者皆非,則為不變的(Invariant)。以泛型來說,如果Banana繼承Fruit,而List視為一種List,則稱List有正變性,如果List視為一種List,則稱List具有逆變性。

Java的泛型不具正變性,所以List不是一種List,因而不能設計show(List fs)來顯示List與List(如果Banana與Apple都繼承自Fruit),但這個需求確實存在,Java中可使用?型態通配字元(Wild card)與extends來宣告變數,使其達到類似正變性,也就是設計show(List fs)來接受List與List。

Java的泛型不具逆變性,所以List不是一種List,這就有個困擾,如果要設計sort函式,希望可以傳入一個Comparator對List或List排序,顯然不能設計為sort(List list, Comparator c),以傳入List為例,這時相當於sort(List list, Comparator c),這樣的話就不能接受Comparator作為第二個參數了。Java可以使用?型態通配字元與super來宣告變數,使其達到類似逆變性,也就是設計sort(List list, Comparator c),以傳入List為例,這時相當於sort(List list, Comparator c),原先設計的Comparator也就可以傳入………然而,這一切實在是太複雜了。

參數多型是為了簡化而不是複雜化
參數多型出發點是為了減輕開發者處理型態的負擔,允許函式設計時不理會參數實際型態,從而設計出介面更一致的函式,並進一步得到編譯時期的型態安全檢查,這份苦差事顯然落到了編譯器頭上,具有強大類型推斷功能的Haskell從參數多型得到了益處,函式設計時彷若動態語言,不用在乎參數型態,但又得到實質的編譯時期型態安全檢查。

Java本身類型推斷功能有限,泛型語法本身亦不簡潔,考慮類別參數化會使得型態系統變得複雜,繼承而衍生出的變異性問題又使得觀念及語法更加魔幻,然而Java泛型不是沒有優點,適當使用泛型確實可以簡化程式,不過在開始涉及Enum>之類的語法,或是面對變異性問題時,也許就應捨棄泛型,因為語法複雜度缺點超越了編譯器給的優點,此時回歸特定多型或次型態多型,或許才是正確處理方式,例如先前排序需求可宣告為sort(List list, Comparator c),在必要時明確進行轉型,反倒是簡潔明確的作法。

 

作者簡介


Advertisement

更多 iThome相關內容