JDK 1.5的泛型實現(Generics in JDK 1.5)
1
侯捷觀點
JDK 1.5的泛型實現
—
Generics in JDK 1.5 —
北京《程序員》
2004/09
台北《Run!PC》2004/09
作者簡介:侯捷,資訊教育、專欄執筆、大學教師。常著文章自娛,頗示己志。
侯捷網站:http://www.jjhou.com(繁體)
北京鏡站:http://jjhou.csdn.net(簡體)
永久郵箱:jjhou@jjhou.com
.
讀者基礎:有
Java語言基礎,使用過
Java Collections。
.
本文適用工具:
JDK1.5
.
本文程式源碼可至侯捷網站下載
http://www.jjhou.com/javatwo-2004-reflection-and-generics-in-jdk15-sample.ZIP
.
本文是
JavaTwo-2004技術研討會同名講題之部分內容書面整理。
.
關鍵術語:
persistence(永續性、持久性)
serialization(序列化、次第讀寫)
generics(泛型)
polymorphism(多型)
全文提要
泛型技術與
Sun JDK的淵源可追溯自
JDK1.3。但無論
JDK 1.3或
JDK1.4,都只是
以編譯器外掛附件的方式來支援泛型語法,並且
Java標準程式庫未曾針對泛型全
侯捷觀點
Generics in JDK 1.5
面改寫。而今
JDK1.5正式納入泛型。本文討論
JDK1.5的泛型實現,包括如何使
用及自訂
generic classes and algorithms,其中若干語法異於
JDK 1.3和
1.4。
我
我我我我曾經在
JavaTwo 2002大會上針對泛型技術給出一個講題,並將內容整理為《Java
泛型技術之發展》一文(http://www.jjhou.com/javatwo-2002.htm)。該文所談的
Java
泛型語法以及
Java泛型技術之內部實作技術,在今天(被
JDK 1.5正式納入)依
然適用。但由於有了若干小變化,並且由於
Java標準程式庫的全面改寫,使我認
為有必要再整理這篇文章,讓讀者輕鬆地在
JDK 1.5中繼續悠遊「泛型」技術。
閱讀本文之前,如果自覺基礎不夠,可以補充閱讀適才提到的《Java泛型技術之
發展》,那是一篇非常完整的文章,可助您完整認識泛型技術的來龍去脈。
Sun JDK的泛型發展歷史要從
1.3版說起。該版本配合
GJ,正式進入泛型殿堂。
所謂
GJ是 "Generic Java" 的縮寫,是一個支援泛型的
Java編譯器補充件,可謂
Java
泛型技術的先趨。隨後,泛型議題正式成為
JSR #14,其技術基礎便是源自
GJ。
JDK1.4搭配
JSR14提供的外掛附件,使泛型技術在
Java世界從妾身未明的身份扶
正而為眾所屬目的焦點。今天,JDK1.5終於內建泛型特性,不僅編譯器不再需要
任何外力(外掛附件)的幫助,整個
Java標準程式庫也被翻新(retrofit),許多
角落針對泛型做了改寫。
讓我們把帶有「參數化型別」(parameterized types)的
classes稱為
generic classes,
把帶有「參數化型別」的
methods稱為
generic algorithms,那麼,對眾多
Java程
式員而言,泛型帶來的影響不外乎以下四點,稍後逐一說明。
..如何使用
generic classes
..如何使用
generic algorithms
..如何自訂
generic classes
..如何自訂
generic algorithms
在此先提醒您,運用泛型時,加上
–Xlint:unchecked編譯選項,可讓編譯器幫
助我們檢查潛在的型別轉換問題。
侯捷觀點
JDK 1.5的泛型實現(Generics in JDK 1.5)3
使用
Generic Classes
Generic classes的最大宗運用是
collections(群集),也就是實作各種資料結構(例
如
list, map, set, hashtable)的那些
classes。也有人稱它們為容器(
containers)。這
些容器被設計用來存放
Object-derived元素。而由於
Java擁有單根繼承體系,任
何
Java classes都繼承自
java.lang.Object,因此任何
Java objects都可以被放進
上述各種容器。換句話說
Java容器是一種異質容器,從「泛型」的字面意義來說,
其實這(原本的設計)才是「泛型」。
然而有時候,而且是大半時候,我們不希望容器元素如此異質化。我們多半希望
使用同質容器。即使用於多型(
polymorphism),我們也希望至少相當程度地規
範容器,令其元素型別為「帶有某種約束」的
base class。例如面對一個準備用來
放置各種形狀(圓圈、橢圓、矩形、四方形、三角形
…)的容器,如果我們能夠
告知這個容器其每個元素都必須是
Shape-derived objects,將相當有助於程式的可
讀性,並減少錯誤,容易除錯,甚至可避免一大堆轉型(
cast)動作。
Java同質容器的語法如下,其中角括號(
<>)的用法和
C++完全相同,角括號之
內的指定型別,就是同質容器的元素型別,如圖
1。
ArrayList<String> strList = new ArrayList<String>();
strList.add("zero");
strList.add("one");
strList.add("two");
strList.add("five");
System.out.println(strList); // [zero, one, two, five]
圖
1 /同質容器的用法。角括號(
<>)內就是元素型別。
下面是另一個實例,程式員要求容器內的每一個元素都必須是「一種形狀」,這
是一種「多型」應用,如圖
2。這些泛型語法自
JDK 1.3+GJ以來不曾改變過。
侯捷觀點
Generics in JDK 1.5
Shape
Object
Stroke Rect Circle
圖
2a /典型的 "Shape"多型繼承體系。
//假設
Stroke, Rect, Circle皆繼承自
Shape
LinkedList<Shape> sList = new LinkedList<Shape>();
sList.add(new Stroke(…));
sList.add(new Rect(…));
sList.add(new Circle(…));
圖
2b /令容器內含各種
Shape元素,並加入一個
Stroke,一個
Rect和一個
Circle。
Li
LiLiLiLink
nknknknked
ededededLis
LisLisLisList
tttt<Shape> sL
sLsLsList
tistisistt;
;;;;
<S
<S<S<Sha
hahahape
pepepe>
>>> sLis
18
2b
18
2c
5
18
2c
22
11
33
44
Ci
CiCiCiCirc
rcrcrcrcle
lelelele
Re
ReReReRect
ctctctct
66
55
77
St
StStStStro
rorororoke
kekekeke
圖
2c /圖
2b程式碼所製造的結果。
Boxing和
Un-boxing帶來的影響
前面曾經說過,任何
Java objects都可以被放進各種容器內。但是
Java基本數值型
別(primitive types,例如
int, double, long, char)並不是一種
class,而數值也談
不上是個
object。如果要把這一類數值放進容器內,必須將容器元素宣告為基本型
別所對應的外覆類別(wrapper classes),例如圖
3。這實在是非常不方便。JDK1.5
侯捷觀點
JDK 1.5的泛型實現(Generics in JDK 1.5)5
新增自動
boxing(封箱)和
un-boxing(拆箱)特性,也就是在必要時刻自動將數
值轉為外覆物件,或將外覆物件轉為數值。有了這項特性,我們可以將圖
3改寫
為圖
4,那就方便多了。
LinkedList<Integer> iList = new LinkedList<Integer>();
iList.add(new Integer(0));
iList.add(new Integer(1));
iList.add(new Integer(5));
iList.add(new Integer(2));
圖
3 /容器元素必須是
object,不可是數值,所以必須使用外覆型別(
wrapper)。
LinkedList<Integer> iList = new LinkedList<Integer>();
iList.add(0); //boxing
iList.add(1);
iList.add(5);
iList.add(2);
int i = iList.get(2); //un-boxing
圖
4 / JDK1.5新增的
boxing/un-boxing特性,使得以方便地將數值放進容器。
使用
Generic Algorithms
在
Java程式庫中,針對容器而設計的
algorithms並不多(不像
C++ 標準程式庫所
提供的那麼多),它們都被置於
java.util.Collections內以
static methods的形
式呈現,例如
sort() , max(), min(), copy(), fill()。圖
5是兩個運用實例,其
語法和
C++完全相同:使用
generic algorithms時並不需要以角括號(
<>)為「參
數化型別」做任何具體指定。這種泛型語法自
JDK1.3+GJ以來不曾改變過。
String str = Collections.max(strList); //strList見前例(圖
1)
Collections.sort(strList);
圖
5 /運用
max()和
sort()
自訂
Generic Classes
先前的
LinkedList<T> 運用實例中,我曾假設
Stroke, Rect, Circle皆繼承自
Shape。如果我們希望這些
classes有足夠的彈性,讓用戶得以在運用這些
classes
時才指定其內部數據(長、寬、半徑等等)的型別,那就得用上泛型語法,如圖
6,
侯捷觀點
Generics in JDK 1.5
而先前的運用實例也得對應地修改為圖
7。
public abstract class Shape {
public abstract void draw();
}
public class Rect<T> extends Shape
implements Serializable {
T m_left, m_top, m_width, m_height;
public Rect(T left, T top, T width, T height ) { ... }
...
}
public class Circle<T> extends Shape
implements Serializable {
T m_x, m_y, m_r;
public Circle(T x, T y, T r) { ... }
...
}
public class Stroke<W,T> extends Shape
implements Serializable {
W m_width;
ArrayList<T> m_ia;
public Stroke(W width, ArrayList<T> ia) { ... }
...
}
圖
6 /自訂
generic classes。本圖實現圖
2a的繼承體系,並以「參數化型別」(圖
中灰色的
T,W等等)代表各
classes內的數據型別。
LinkedList<Shape> sList = new LinkedList<Shape>();
sList.add(new Stroke<Integer,Integer>(…));
sList.add(new Rect<Integer>(…));
sList.add(new Circle<Integer>(…));
圖
7 /容器的每個元素型別都是
generic classes,所以製造元素時必須使用泛型語
法(角括號)。請與圖
2b比較。
圖
6和圖
7的泛型語法自
JDK1.3+GJ以來不曾改變過。它迥異於
C++,後者要求
程式必須在
class名稱前加上語彙單元
template<>,藉此告訴編譯器哪些符號是
型別參數(
type parameters),如圖
8。
template <typename T>
class Rect : public Shape
侯捷觀點
JDK 1.5的泛型實現(Generics in JDK 1.5)7
{
private:
T m_left, m_top, m_width, m_height;
public:
Rect(T left, T top, T width, T height ) { ... }
...
}
圖
8 / C++ class必須以
template<typename T>這種語彙單元型式,告訴編譯器
T是個參數化型別。請與圖
6之同名
Java class比較。
現在讓我們看看
Java程式庫源碼,從中學習更多的泛型語法。圖
9a是
java.util.ArrayList的
JDK1.5源碼,圖
9b是其
JDK 1.4源碼,可資比較。
#001 public class ArrayList<E> extends AbstractList<E>
#002 implements List<E>, RandomAccess,
#003 Cloneable, java.io.Serializable
#004 {
#005 private transient E[] elementData;
#006 private int size;
#007 public ArrayList(int initialCapacity) {
#008 super();
#009 // check if (initialCapacity < 0)...
#010 this.elementData = (E[])new Object[initialCapacity];
#011 }
#012
#013 public ArrayList() {
#014 this(10);
#015 }
#016 ...
#017 }
圖
9a / JDK1.5的
java.util.ArrayList源碼
#001 public class ArrayList extends AbstractList
#002 implements List, RandomAccess,
#003 Cloneable, java.io.Serializable
#004 {
#005 private transient Object elementData[];
#006 private int size;
#007 public ArrayList(int initialCapacity) {
#008 super();
#009 // check if (initialCapacity < 0) ...
#010 this.elementData = new Object[initialCapacity];
#011 }
#012
侯捷觀點
Generics in JDK 1.5
#013 public ArrayList() {
#014 this(10);
#015 }
#016 ...
#017 }
圖
9b / JDK1.4的
java.util.ArrayList源碼
從圖
9a可以看出,參數型別(圖中的
E)不但可以繼續被沿用做為
base class或
base
interfaces的參數型別,也可以出現在
class定義區內「具體型別可以出現」的任何
地方。不過,例外還是有的,例如這一行:
#010 this.elementData = (E[])new Object[initialCapacity];
不能寫成:
#010 this.elementData = new E[initialCapacity];
那會出現
generic array creation error.
自訂
Generic Algorithms
定義於任何
classes內的任何一個
static method,你都可以說它是個
algorithm。如
果這個
method帶有參數化型別,我們就稱它是
generic algorithm。例如:
//在某個
class之內
public static <T> T gMethod (List<T> list) { ... }
這種語法和
generic classes有相當程度的不同:泛型符號
<T> 必須加在
class名稱
之後,卻必須加在
method名稱(及回傳型別)之前。
JDK 1.5比以前版本增加了更多彈性,允許所謂
bounded type parameter,意指「受
到更多約束」的型別參數。下例表示
gMethod()所收到的引數不但必須是個
List,
而且其元素型別必須實作
Comparable:
public static <T extends Comparable<T>> T gMethod (List<T> list)
{ ... }
這種「受到更多約束」的型別參數寫法,雖然不存在於
JDK1.4+JSR14,但其實原
本存在於
JDK1.3+GJ中,只不過用的是另一個關鍵字:
侯捷觀點
JDK 1.5的泛型實現(Generics in JDK 1.5)9
public static <T implements Comparable<T>> T gMethod (List<T> list)
JDK 1.5還允許將「不被
method實際用到」的型別參數以符號 '?' 表示,例如:
public static List<?> gMethod (List<?> list)
{
return list; //本例簡單地原封不動傳回
}
此例
gMethod()接受一個
List(無論其元素型別是什麼),傳回一個
List(無論
其元素型別是什麼)。由於不存在(或說不在乎)型別參數(因為
method內根本
不去用它),也就不必如平常一般在回傳型別之前寫出
<T>來告知編譯器了。
上面這個例子無法真正表現出符號 '?' 的用途。真正的好例子請看
JDK1.5的
java.util.Collections源碼,見圖
10a。圖
10b則是其
JDK1.4源碼,可資比較。
請注意,例中的 '?' 不能被替換為任何其他符號。圖
10a程式碼所描述的意義,
請見圖
11的細部解釋。
#001 public class Collections
#002 ...
#003 public static
#004 <T extends Object & Comparable<? super T>>
#005 T max(Collection<? extends T> coll) {
#006 Iterator<? extends T> i = coll.iterator();
#007 T candidate = i.next();
#008
#009 while(i.hasNext()) {
#010 T next = i.next();
#011 if (next.compareTo(candidate) > 0)
#012 candidate = next;
#013 }
#014 return candidate;
#015 }
#016 ...
#017 } // of Collections
圖
10a / JDK1.5的
java.util.Collections源碼。
#001 public class Collections
#002 ...
#003 public static
#004 //這裡我刻意放空一行,以利與
JDK1.5源碼比較
侯捷觀點
Generics in JDK 1.5
#005 Object max(Collection coll) {
#006 Iterator i = coll.iterator();
#007 Comparable candidate = (Comparable)(i.next());
#008
#009 while(i.hasNext()) {
#010 Comparable next = (Comparable)(i.next());
#011 if (next.compareTo(candidate) > 0)
#012 candidate = next;
#013 }
#014 return candidate;
#015 }
#016 ...
#017 } // of Collections
圖
10b / JDK1.4的
java.util.Collections源碼。
3 4 5
<T extends Object & Comparable<? super T>>
T max(Collection<? extends T> coll) {
1 26
1. max()接收一個
Collection object。
2.該
Collection object所含元素必須是
T-derived object。
3. T必須繼承
Object(這倒不必明說,因為
Java必定如此)。
4. T必須實作
Comparable。
5. Comparable所比較的型別必須是
T的
super type。
6. max()傳回
T object。
圖
11 /本圖詳細說明圖
10a的怪異內容(#4, #5兩行)
面對圖
11如此「怪異而罕見」的語法,給個實際用例就清楚多了:
LinkedList<Shape> sList = new LinkedList<Shape>();
...
Shape s = Collections.max(sList);
我們讓
Collections.max()接受一個先前曾經說過的
LinkedList<Shape>(見圖
2a,b),那是個
Collections object(符合圖
11條件
1),其中每個元素都是
Shape-derived objects(符合圖
11條件
2),因此本例中的
T就是
Shape。Shape的
確繼承自
Object(符合圖
11條件
3),並且必須實作
Comparable(才能符合圖
11
侯捷觀點
JDK 1.5的泛型實現(Generics in JDK 1.5)11
條件
4),而被比較物的型別必須是
Shape的
super class(才能符合圖
11條件
5)。
max()比較所得之最大值以
Shape表示(符合圖
11條件
6)——這是合理的,因
為不知道比較出來的結果會是
Rect或
Circle或
Stroke,但無論如何它們都可以
向上轉型為
Shape。
為了完成上述的條件
4和條件
5,先前的
Shape必須修改,使得以被比較。也就是
說
Shape必須實作
Comparable介面,如圖
12,其中針對
compareTo()用上了典
型的
Template Method設計範式(design pattern),再令每一個
Shape-derived classes
都實作
L()如圖
13,這就大功告成了。
public abstract class Shape implements Comparable<Shape> {
...
public abstract double L(); //計算周長
public int compareTo(Shape o) { //假設「以周長為比較依據」合理!
return (this.L() < o.L() ? -1 : (this.L() == o.L() ? 0 : 1));
}
}
圖
12 /修改圖
7的
Shapeclass
public double L() {
... 計算周長
//這裡有點學問,見最後「擦拭法帶來的遺憾」討論。
return xxx; //傳回周長
}
圖
13 /每個
Shape-derived classes都必須實作出如此型式的
L()。
參數化型別(Parameterized type)存在多久?
究竟
generic classes所帶的參數化型別,在編譯後是否還保留?或者說,假設我們
將一個元素型別為
Integer的
LinkedList容器寫入檔案,而後讀出並恢復「先前
被
serialized(序列化)至檔案」的容器,如圖
14。此時我們的第一個考慮作法或
許如下(和最初的宣告完全相同):
LinkedList<Integer> iList2 = (LinkedList<Integer>)in.readObject();
但
JDK1.5編譯器發出警告,告訴我們 "unchecked cast"(JDK1.4則直接抱怨它是
錯誤的)。改成這樣情況亦同:
LinkedList iList2 = (LinkedList<Integer>)in.readObject();
侯捷觀點
Generics in JDK 1.5
看來編譯器面對即將被
deSerialized(反序列化)讀得的型別資訊,似乎無法判別
是否可以成功轉型為
LinkedList<Integer>。另一種寫法是:
LinkedList<Integer> iList2 = (LinkedList)in.readObject();
這一次
JDK1.5編譯器發出的警告訊息是:
"unchecked conversion"。改為這樣更非
警告可以善了:
LinkedList<Integer> iList2 = in.readObject();
對此
JDK1.5編譯器會直接報錯:
"incompatible types"。如果改成這樣:
LinkedList iList2 = (LinkedList)in.readObject();
這才是既無錯誤又無警告的完美寫法。
LinkedList<Integer> iList = new LinkedList<Integer>();
...
ObjectOutputStream out =
new ObjectOutputStream(
new FileOutputStream("out"));
out.writeObject(iList); //寫入
out.close();
ObjectInputStream in =
new ObjectInputStream(
new FileInputStream("out"));
//…這裡準備進行讀取動作
readObject()
圖
14 /將元素型別為
Integer的容器寫入檔案,而後準備讀出。
由以上測試結果可以預期,似乎存在這一事實:當
object被寫入檔案,即失去其
泛型型別參數(如果有的話)。因此讀回的只是「非泛型」的
class資訊。如果上
述最後那個「完美」寫法改成這樣:
ArrayList iList2 = (ArrayList)in.readObject();
仍可順利編譯,在編譯器的眼裡看來也很完美,但執行期會出現異常,告知「讀
入的
class資訊」和「程式預備接收的
class資訊」不相符合。異常訊息如下:
Exception in thread "main" java.lang.ClassCastException:
侯捷觀點
JDK 1.5的泛型實現(Generics in JDK 1.5)
13
java.util.LinkedList
我們可以觀察
serialization的輸出檔獲得證據。從圖
15可以看出來,檔案內記錄
的
class名稱是
java.util.LinkedList,並一併記錄了元素型別
java.lang.Integer(及其
base class java.lang.Number)。但元素型別其實是針對每一個元素都會記
錄的(當然啦,如果遇上相同型別的元素,這些資訊並不會又被傻傻地完整記錄
一遍,那太浪費時間和空間,而是只記錄一個
handle,詳見《Java的物件永續之
道》,網址列於文後)。這些記錄對於
deSerialization過程中恢復容器原型和內容
有其必要,但無法讓編譯器推論當初使用的是
LinkedList容器或是
LinkedList<Integer>容器。
000000: AC ED 00 05 73 720014 6A6176 61 2E 75 74 69秒..sr..ja
jajajajava
vavavava.
...uti
iutiututii
.ut
000010: 6C 2E 4C 69 6E 6B 65 64 4C 69 73 74 0C 29 53 5D l.
l.l.l.l.Li
LiLiLiLink
nknknknked
ededededLi
LiLiLiList
stststst.)S]
000020: 4A 60 88 22 03 00 00 78 70 77 04 00 00 00 04 73 J`."...xpw.....s
000030: 72 00 11 6A 61 76 61 2E 6C 61 6E 67 2E 49 6E 74 r..ja
jajajajava
vavavava.
...la
lalalang
ngngng.
...Int
tIntInIntt
`"
.lang.In
000040: 65 67 65 72 12 E2 A0 A4 F7 81 87 38 02 00 01 49 eg
egegegeger
erererer.?父?8...I
000050: 00 05 76 61 6C 75 65 78 72 00 10 6A 61 76 61 2E ..valuexr..ja
jajajajava
vavavava.
....
000060: 6C 61 6E 67 2E 4E 75 6D 62 65 72 86 AC 95 1D 0B la
lalalalang
ngngngng.N
.N.N.N.Num
umumumumbe
bebebeber
rrrr ...
圖
15 /將元素型別為
Integer的容器寫入檔案,而後準備讀出。
Java擦拭法 vs. C++膨脹法
為什麼
Java容器的參數化型別無法永續存在於檔案(或其他資料流)內?本文一
開始已經說過,這些容器被設計用來存放
Object-derived元素,而
Java擁有單根
繼承體系,所有
Java classes都繼承自
java.lang.Object,因此任何
Java objects
都可以被放進各種容器,換句話說
Java容器本來就是一種「泛型」的異質容器。
今天加上參數化型別反而是把它「窄化」了。「泛型」之於
Java,只是一個角括
號面具(當然這個面具帶給了程式開發過程某些好處);摘下面具,原貌即足夠
應付一切。因此
Java使用所謂「擦拭法」來對待角括號內的參數化型別,如圖
16a。
下面是「擦拭法」的四大要點(另有其他枝節,本文不談):
.
一個參數化型別經過擦拭後應該去除參數(於是
List<T>被擦拭成為
List)
.
一個未被參數化的型別經過擦拭後應該獲得型別本身(於是
Byte被擦拭成為
Byte)
.
一個型別參數經過擦拭後的結果為
Object(於是
T被擦拭後變成
Object)
侯捷觀點
Generics in JDK 1.5
.
如果某個
method call的回傳型別是個型別參數,編譯器會為它安插適當的轉型
動作。
這種觀念和
C++的泛型容器完全不同。
C++容器是以同一套程式碼,由編譯器根
據其被使用時所被指定的「不同的參數化型別」建立出不同版本。換句話說一份
template(範本、模板)被膨脹為多份程式碼,如圖
16b。
java.util.LinkedList.java
public class LinkedList<E>
{ ... };
Java
擦拭法
public class LinkedList
{ ... };
import java.util.*
LinkedList ...;
LinkedList ...;
LinkedList ...;
.class
.class
import java.util.*
LinkedList<Integer> ...;
LinkedList<String> ...;
LinkedList<Double> ...;
圖
16a / Java以擦拭法成就泛型
template <typename T,...>
class list { ... };
#include <list>
list<int> li;
list<string> ls;
list<double> ld;
list
C++
class list; // string版本
list<int> li;
list<string> ls;
list<double> ld;
class list; // int版本
class list; // double版本
.exe
膨脹法
圖
16b / C++以膨脹法成就泛型
擦拭法帶來的遺憾
先前談到的 "Shape"多型實例(圖
2a),其中的
class Rect:
public class Rect<T> extends Shape
implements Serializable {
T m_left, m_top, m_width, m_height;
public Rect(T left, T top, T width, T height ) { ... }
...
}
經過擦拭後變成了:
public class Rect extends Shape
侯捷觀點
JDK 1.5的泛型實現(Generics in JDK 1.5)
15
implements Serializable {
Object m_left, m_top, m_width, m_height;
public Rect(Object left, Object top, Object width, Object height )
{ ... }
...
}
這麼一來,任何數值運算,例如先前提過的「周長計算」
L()將無法編譯,如圖
17:
//class Rect<T>內
public double L() {
return (double)((m_width + m_height) * 2);
}
圖
17 / L()發生錯誤
錯誤訊息是:
"operator + cannot be applied to T,T"。是的,兩個
Object object如何
相加呢?
Java並沒有提供像
C++ 那樣的運算子重載(
operator overloading)功能!
可以說,圖
2a的
Shape繼承體系只是介面正確,一旦面臨某些情況,卻無實用性。
我的結論是,將參數化型別用於
Java non-collection classes身上,恐怕會面臨許多
束縛。(註:讀者來函提供了此問題的一個解答,見本文末尾添加之補充)
更多資訊
以下是與本文主題相關的更多討論。這些資訊可以彌補本文篇幅限制而造成的不
足,並帶給您更多視野。
.
《Java泛型技術之發展》
,by侯捷。
http://www.jjhou.com/javatwo-2002-genericsin-
jdk14.pdf
.
《
Java的物件永續之道》
,by侯捷。
http://www.jjhou.com/javatwo-2003serialization-
doc.pdf
侯捷觀點
Generics in JDK 1.5
■補充
讀者
AutoWay針對無法計算周長這個問題,來信如下:
From: AutoWay
Sent: Monday, January 17, 2005 7:57 PM
Subject: 《JDK 1.5的泛型實現》讀者回應
侯捷兄:隨函寄上Rect.java程式之修訂,俾可以進行「周長計算」。修訂內容如
下:在設定type parameter時,同時宣告其type bound,例如本例修改為<Textends
Number>。系統進行編譯時,就知道T是Number或其subclass;因此內部程式就
可運用Number提供的methods進行運算了。
我想,這就是type parameters之所以提供type bounds機制的主要原因。如果type
bounds光用來限制type arguments之傳遞,實在沒啥意思!感謝本文揭示的例子,
讓我對type bounds有進一步的省思與認識;若有謬誤,亦請來信指教。
侯捷回覆:非常感謝
AutoWay兄的指正,解除了我的盲點。整理於下。圖
6之程
式碼應改為:
public class Rect<T extends Number> extends Shape
implements Serializable {
...
}
public class Circle<T extends Number> extends Shape
implements Serializable {
...
}
public class Stroke<W extends Number,T extends Number> extends Shape
implements Serializable {
...
}
圖
17程式碼應改為:
//class Rect<T extends Number>內
public double L() {
return (m_width.doubleValue() + m_height.doubleValue()) * 2;
}
另兩個
classes(Circle和
Stroke)同理修改。
侯捷觀點