2009年12月1日 20:35

從 C++ Template 到 Java Generic,一步一步來

Java 實作了泛型(generic)機制以實現 C++ 樣板(template) 的一部份能力,兩者的語法乍看之下也有些相似。 雖然我覺得 C++ 樣板很難搞,而且兩者的語法有點像,但是相較於完全陌生的 Java 泛型,我用起 C++ 樣板來還是比較熟練的。很自然的,當我試圖要用 Java 的泛型重構程式碼時,我會先從 C++ 樣板的觀點來思考。

我將日前工作中碰到的一段我想用泛型重構的程式碼,取其大綱出來練習。本文紀錄了大致的改寫過程。


先用 C++ 樣板打草稿

我比較熟悉 C++ 樣板(template),所以在動手使用 Java 泛型(generic)之前,我還是習慣先用 C++ 樣板來打稿,讓我構想程式結構。

有 N, M, S 三個類別,這三個類別沒有繼承關係。但是在操作形式上卻有相當高的重複性,很多地方只是型別不同,程式結構完全一樣。幾乎是剪貼、複製後,再代換型別名稱就完工的情形。正是泛型派得上用場的情形。樣板類別 Cx 就是重複的程式碼內容。

Revision 1

第一步,先按 Java 的規則,將每個類別打散到個別的源碼文件中。 將 C++ 的類別定義語法改寫成 Java 的類別定義語法。將樣板(template)改成 Java 的泛型(generic)語法。

Cx.java
N.java
M.java
S.java
Main.java

類別 N, M, S 編譯都沒問題,但是編譯 Cx 時 javac 告訴我不知道 data.value這個符號是什麼?

這時,我才想到 Java 支援的是泛型而不是樣板,這兩者果然是不同的。

樣板基本上是把整個定義內容視為一個程式碼原型,編譯時再將參數中列出的類別符號,代換掉程式碼中的符號。我們可以把這個動作比擬為 script 對字串的竄寫動作。我用 PHP 來模擬示範。

輸出結果是一個新的類別的程式碼。

C++ 編譯器(或者是前置處理器)先將樣板內容進行如上所模擬的代換動作,產生新的類別程式碼,再將生成的程式碼交給一般程式碼編譯單元(或者是後端的 C compiler)編譯成目的碼(object code)。

但是 Java 提供的是泛型而不是樣板,所以它無法這麼簡單地完成型別參數的代換動作。 泛型只是告訴 javac ,我們會將類別當成參數傳遞過來,這些類別之間可能沒有繼承關係,但是卻共用一組一般化的程式碼進行演算。 再者,如果我們在一般化的程式碼中,要調用參數化型別之實體的方法,那麼我們也必須告訴 javac 一個一般化、泛型化的類別定義,這樣 javac 才會知道這個參數化型別大概長什麼樣子、有哪些方法。

Revision 2

在第一個修改版本中,我只告訴 javac: "data 的型別將會由 DataType 參數傳遞過來"。 但我沒有告訴它 DataType 大概長什麼樣子,所以它無從尋找 data.value 這個符號。

我們必須再定義一個東西,這個東西至少要有 value() 方法。 而我們要把這個東西當成 DataType 參數化型別的泛型、一般化內容。 這個東西只要有個輪廓,並不需要有任何實際的內容,用介面(interface) 或抽象類別(abstract class)都可。

我再加一個介面 IDataType 作為 DataType 的泛型吧。


Revision 3

哎呀,我在宣告 DataType 時,沒有考慮到 data.value 用於 DataType.getData() 的回傳值,只是隨手寫了 int 作為 IDataType.value() 回傳型態。於是 javac 又說找到 int 但要的是 ReturnType,兩者型態不符合。

但是 ReturnType 是另一個參數化的型別,顯然我得把 ReturnType 這個參數再傳給 IDataType 才行。這就要把 IDataType 也變成另一個泛型,具有一個型別參數。 接著修改 Cx,把 Cx 泛型定義中的 ReturnType 再傳給 IDataType<?>


Ok, 這次 javac 沒再抱怨了,泛型的主要內容沒錯。接下來編譯 Main。

不接受 int 類別作為參數,但是接受 String 類別作為參數。

Revision 4

喔喔,我又忘了 Java 沒有把原始型態和參考型態一視同仁,泛型不支援原始型態。 所以原始型態的 int 要改成參考型態的 Integer 。

type parameter ? is not within its bound 又是什麼意思?

Revision 5

回顧一下 Cx 泛型的內容,我告訴 javac: "參數 DataType 的泛型是 IDataType 介面"。 如此一來就給了一個限制條件,將 DataType 可以接受的類型侷限在實作了 IDataType 的類別。但是類別 N, M, S 並未宣告它們實作了 IDataType 介面,所以不在 DataType 可接受的範圍中。因此我要再修改類別 N, M, S 的內容,加上 IDataType 的宣告。

原本這三個類別之間沒有關係,但改寫至此, Java 強迫我們拉上關係,讓這三個類別實現了同一個介面。此非我所願,幸好在這個範例中的影嚮不大。得過且過吧。



$ javac *.java
$ java Main
-1
10
hello

這次大功告成,我終於如願以償地寫了一個 Java 的泛型類別... 差點忘了還有一個泛型介面。 同時,我也覺得 C++ 樣板沒那麼難了。

Java 的泛型語法不改要程序員先跳過火圈才能吃到香蕉的本色,我只跳了兩個圈圈就重構完成這個很單純的範例程式。 不過現實可沒那麼輕鬆,至少在我日前負責的案子中,有幾處地方我就放棄用泛型去重構它們,那簡直是自討苦吃。舉個例子來說,我想在 Cx 泛型中增加一個無參數的預設建構子,如下列:

我增加了第4~6行的預設建構子,看起來非常簡單、非常合理、不應該受到任何刁難的需求,但是 javac 高舉手中的法杖發出刺目紅光對我大喊: Unexcepted type!。我就是不可以直接 new 一個參數化型別的實例。但是 C++ 樣板可以這麼做,一點都不廢話。

反正案子快結了,也沒人關心軟體內部是不是充斥太多重複的程式碼。至少我沒省略測試案例,天天都跑一次 AllTests 和 Nightly build ,交給客戶的軟體外在品質合格,也就夠了。既然 Java 語言並沒有提供靈活的方法讓我們輕鬆地進行重構工作,還是算了吧,早點下班比較實在。


  • shirock 發表於樂多回應(4)引用(3)C/C++/C#/Java編輯本文
    樂多分類:學術/學習 │昨日人次:4 │累計人次:2609 │標籤:template, generic, 樣板, 泛型
    Ads by Roodo! 

    引用URL

    http://cgi.blog.roodo.com/trackback/10890551
    引用列表:
    我建議你像我一樣,在不必交付給客戶的地方,儘可能用適當的語言來解決問題。 Ford 對此有一個很好的說法。
    與 metavige 和 alexchen 對話 Java 語言【石頭閒語】 at 2009年12月4日 00:48
    四篇文章其實在講同一件事。只是分別用不同的程式語言來做。你可以挑自己熟悉的語言來看。
    以不同語言的觀點來看 C++ template【石頭閒語】 at 2009年12月7日 01:02
    表面上看起來好像實作泛型可以讓某一段程式碼重複使用,但 Java 在泛型的限制,也增加他重構程式碼的困難度與複雜度。這麼說來,假如石頭成的想法是正確的,用 Java 的泛型來重構程式碼,只會讓程式員沒事自討苦吃。然而,同人在仔細研究他的程式碼之後,發現可以用更簡潔的方式來使用 Java 的泛型。 SHARETHIS.addEntry({ title: "", url: "" });...
    Java 泛型複雜嗎?【同人的生活派對】 at 2009年12月17日 13:04
    回應文章
    看你的文章,的確有很多值得深省的地方

    其實我重頭看你的作法,我直覺就會想到用 Strategy Pattern 的方式做
    用繼承的方式,而不是委託

    這的確就與 C++ Template 那種方式,就有不一樣的意義了
    不過,不一樣的語言,就會有不同的概念
    因為 Generic Type 本來就不是在做 Template 的
    是以 OO 的觀點來做到不 DRY ~ 這是我自己的理解
    | 檢舉 | Posted by metavige at 2009年12月1日 22:50
    如果N, M, S 互不相干,那把它們透過同一個 template 引用的用意何在?

    而既然用了同一個 template 引用,我覺得N, M, S其實在某些方面就是相干的,而這相關性,可能就是原著透過 refactor 方式找出來的 IDataType。只是 C++ 透過 template 置換的方法,忽略掉這個事實罷了。

    設想在 C++ 的版本中,有人把 M 的 value 方法改成 data 方法,則你的程式非得連結到 Cx 才會在 compile time 出現錯誤訊息。但 Java 的版本,因為有 IDataType 的限制,即使沒有 Cx 類別,也是可以很快的找出錯誤所在。就強固性而言,C++ 這點就比較不好了。

    另外一個考量,也不一定要為了 template 而template,如果在 Cx 中有相同的程式碼,最簡單的作法,就是把 Cx 變成 helper 類別直接在 N, M, S 中引用。若是情況合理,也可考慮作成共用父類別。
    | 檢舉 | Posted by Edward at 2009年12月8日 13:18
    walk like a duck, it is a duck.

    C++寫起來真的粉爽~~ ^^
    | 檢舉 | Posted by vs at 2011年11月16日 00:10
    vs只講了一半啊。C++ template 在使用上具有天堂與地獄的兩極特性。
    單純地使用現成的 template library 非常爽快。
    但是要自己寫一個 template library 卻很要命。
    | 檢舉 | Posted by 遊手好閒的石頭成 at 2011年11月22日 15:55