Java 實作了泛型(generic)機制以實現 C++ 樣板(template) 的一部份能力,兩者的語法乍看之下也有些相似。
雖然我覺得 C++ 樣板很難搞 ,而且兩者的語法有點像,但是相較於完全陌生的 Java 泛型,我用起 C++ 樣板來還是比較熟練的。很自然的,當我試圖要用 Java 的泛型重構程式碼時,我會先從 C++ 樣板的觀點來思考。
我將日前工作中碰到的一段我想用泛型重構的程式碼,取其大綱出來練習。本文紀錄了大致的改寫過程。
先用 C++ 樣板打草稿
我比較熟悉 C++ 樣板(template),所以在動手使用 Java 泛型(generic)之前,我還是習慣先用 C++ 樣板來打稿,讓我構想程式結構。
有 N, M, S 三個類別,這三個類別沒有繼承關係。但是在操作形式上卻有相當高的重複性,很多地方只是型別不同,程式結構完全一樣。幾乎是剪貼、複製後,再代換型別名稱就完工的情形。正是泛型派得上用場的情形。樣板類別 Cx 就是重複的程式碼內容。
#include <iostream>
using namespace std ;
template < class DataType , class ReturnType >
class Cx {
private:
DataType data ;
public:
Cx ( DataType & v ) { data = v ; }
ReturnType getData () {
return data . value ();
}
};
class N {
private:
int n ;
public:
N () { n = 0 ; }
N ( int v ) { n = v ; }
int value () {
return - n ;
}
};
class M {
private:
int m ;
public:
M () { m = 0 ; }
M ( int v ) { m = v ; }
int value () {
return m * 10 ;
}
};
class S {
private:
string s ;
public:
S () { s = "" ; }
S ( const char * v ) { s = v ; }
string value () {
return s ;
}
};
int main () {
N n = N ( 1 );
Cx < N , int >* cn = new Cx < N , int > ( n );
cout << cn -> getData () << endl ;
M m = M ( 1 );
Cx < M , int >* cm = new Cx < M , int > ( m );
cout << cm -> getData () << endl ;
S s = S ( "hello" );
Cx < S , string >* cs = new Cx < S , string > ( s );
cout << cs -> getData () << endl ;
return 0 ;
}
Revision 1
第一步,先按 Java 的規則,將每個類別打散到個別的源碼文件中。
將 C++ 的類別定義語法改寫成 Java 的類別定義語法。將樣板(template)改成 Java 的泛型(generic)語法。
Cx.java
//revision: 1
public class Cx < DataType , ReturnType > {
private DataType data ;
public Cx ( DataType v ) {
data = v ;
}
public ReturnType getData () {
return data . value ();
}
};
N.java
//revision: 1
public class N {
private int n ;
public N () { n = 0 ; }
public N ( int v ) { n = v ; }
public int value () {
return - n ;
}
};
M.java
//revision: 1
public class M {
private int m ;
public M () { m = 0 ; }
public M ( int v ) { m = v ; }
public int value () {
return m * 10 ;
}
};
S.java
//revision: 1
public class S {
private String s ;
public S () { s = "" ; }
public S ( String v ) { s = v ; }
public String value () {
return s ;
}
};
Main.java
//revision: 1
public class Main {
public static void main ( String [] args ) {
N n = new N ( 1 );
Cx < N , int > cn = new Cx < N , int >( n );
System . out . println ( cn . getData () );
M m = new M ( 1 );
Cx < M , int > cm = new Cx < M , int >( m );
System . out . println ( cm . getData () );
S s = new S ( "hello" );
Cx < S , String > cs = new Cx < S , String >( s );
System . out . println ( cs . getData () );
}
}
類別 N, M, S 編譯都沒問題,但是編譯 Cx 時 javac 告訴我不知道 data.value
這個符號是什麼?
public ReturnType getData () {
return data . value ();
//error: ^ cannot find symbol
}
這時,我才想到 Java 支援的是泛型而不是樣板,這兩者果然是不同的。
樣板基本上是把整個定義內容視為一個程式碼原型,編譯時再將參數中列出的類別符號,代換掉程式碼中的符號。我們可以把這個動作比擬為 script 對字串的竄寫動作。我用 PHP 來模擬示範。
<? php
//Cx<N, int>* cn = new Cx<N, int>( n );
$ DataType = 'N' ;
$ ReturnType = ' int ' ;
//template<class DataType, class ReturnType>
$ template_DataType_ReturnType =<<< CX
class Cx__ $ { DataType } __ $ { ReturnType } { //模擬 template 產生的新類別簽名
private:
$ { DataType } data ;
public:
Cx ( $ { DataType } & v ) { data = v ; }
$ { ReturnType } getData () {
return data . value ();
}
};
CX ;
echo $ template_DataType_ReturnType
?>
輸出結果是一個新的類別的程式碼。
class Cx__N__int { //模擬 template 產生的新類別簽名
private:
N data ;
public:
Cx ( N & v ) { data = v ; }
int getData () {
return data . value ();
}
};
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: 2
public interface IDataType {
public int value ();
}
//revision: 2
public class Cx < DataType extends IDataType , ReturnType > {
private DataType data ;
public Cx ( DataType v ) {
data = v ;
}
public ReturnType getData () {
return data . value ();
//error: incompatible types
//found : int
//required: ReturnType
}
};
Revision 3
哎呀,我在宣告 DataType
時,沒有考慮到 data.value
用於 DataType.getData()
的回傳值,只是隨手寫了 int
作為 IDataType.value()
回傳型態。於是 javac 又說找到 int
但要的是 ReturnType
,兩者型態不符合。
但是 ReturnType
是另一個參數化的型別,顯然我得把 ReturnType
這個參數再傳給 IDataType
才行。這就要把 IDataType
也變成另一個泛型,具有一個型別參數。
接著修改 Cx,把 Cx 泛型定義中的 ReturnType
再傳給 IDataType<?>
。
// revision: 3
public interface IDataType < ReturnType > {
public ReturnType value ();
}
// revision: 3
public class Cx < DataType extends IDataType < ReturnType >, ReturnType > {
private DataType data ;
public Cx ( DataType v ) {
data = v ;
}
public ReturnType getData () {
return data . value ();
}
};
Ok, 這次 javac 沒再抱怨了,泛型的主要內容沒錯。接下來編譯 Main。
//revision: 1
public class Main {
public static void main ( String [] args ) {
N n = new N ( 1 );
Cx < N , int > cn = new Cx < N , int >( n );
// ^ unexpected type
//found : int
//required: reference
System . out . println ( cn . getData () );
M m = new M ( 1 );
Cx < M , int > cm = new Cx < M , int >( m );
// ^ unexpected type
//found : int
//required: reference
System . out . println ( cm . getData () );
S s = new S ( "hello" );
Cx < S , String > cs = new Cx < S , String >( s );
System . out . println ( cs . getData () );
}
}
不接受 int 類別作為參數,但是接受 String 類別作為參數。
Revision 4
喔喔,我又忘了 Java 沒有把原始型態和參考型態一視同仁,泛型不支援原始型態。
所以原始型態的 int 要改成參考型態的 Integer 。
//revision: 4
public class Main {
public static void main ( String [] args ) {
N n = new N ( 1 );
Cx < N , Integer > cn = new Cx < N , Integer >( n );
// ^ type parameter N is not within its bound
System . out . println ( cn . getData () );
M m = new M ( 1 );
Cx < M , Integer > cm = new Cx < M , Integer >( m );
// ^ type parameter M is not within its bound
System . out . println ( cm . getData () );
S s = new S ( "hello" );
Cx < S , String > cs = new Cx < S , String >( s );
// ^ type parameter S is not within its bound
System . out . println ( cs . getData () );
}
}
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 強迫我們拉上關係,讓這三個類別實現了同一個介面。此非我所願,幸好在這個範例中的影嚮不大。得過且過吧。
//revision: 5
public class N implements IDataType < Integer > {
private Integer n ;
public N () { n = 0 ; }
public N ( Integer v ) { n = v ; }
public Integer value () {
return - n ;
}
};
//revision: 5
public class M implements IDataType < Integer > {
private Integer m ;
public M () { m = 0 ; }
public M ( Integer v ) { m = v ; }
public Integer value () {
return m * 10 ;
}
};
//revision: 5
public class S implements IDataType < String > {
private String s ;
public S () { s = "" ; }
public S ( String v ) { s = v ; }
public String value () {
return s ;
}
};
$ javac *.java
$ java Main
-1
10
hello
這次大功告成,我終於如願以償地寫了一個 Java 的泛型類別... 差點忘了還有一個泛型介面。
同時,我也覺得 C++ 樣板沒那麼難了。
Java 的泛型語法不改要程序員先跳過火圈才能吃到香蕉的本色,我只跳了兩個圈圈 就重構完成這個很單純的範例程式。
不過現實可沒那麼輕鬆,至少在我日前負責的案子中,有幾處地方我就放棄用泛型去重構它們,那簡直是自討苦吃。舉個例子來說,我想在 Cx 泛型中增加一個無參數的預設建構子,如下列:
public class Cx < DataType extends IDataType < ReturnType >, ReturnType > {
private DataType data ;
public Cx () {
data = new DataType ();
}
public Cx ( DataType v ) {
data = v ;
}
public ReturnType getData () {
return data . value ();
}
};
我增加了第4~6行的預設建構子,看起來非常簡單、非常合理、不應該受到任何刁難的需求,但是 javac 高舉手中的法杖發出刺目紅光對我大喊: Unexcepted type! 。我就是不可以直接 new 一個參數化型別的實例。但是 C++ 樣板可以這麼做,一點都不廢話。
反正案子快結了,也沒人關心軟體內部是不是充斥太多重複的程式碼。至少我沒省略測試案例,天天都跑一次 AllTests 和 Nightly build ,交給客戶的軟體外在品質合格,也就夠了。既然 Java 語言並沒有提供靈活的方法讓我們輕鬆地進行重構工作,還是算了吧,早點下班比較實在。
相關文章
樂多舊網址: http://blog.roodo.com/rocksaying/archives/10890551.html
樂多舊回應