新版的J2SE 5.0 Tiger引進數個在Java Community Process下發展與泛型(Generics)相關的幾個新特色功能。我們先從一般性的觀念開始進入主題,然後逐步地介紹泛型、相關流程控制的改進、搭配泛型的Collection…等。說明的重點在於瞭解先前我們所面臨的問題,然後再討論這幾個新特色功能怎樣著手處理,並且搭配一些範例程式碼來使你熟悉它們的用法。
一開始我們先以「傳值」的問題來作為說明的切入點。所有的程式語言,不管它是根據程序性的、物件基礎的,或物件導向的設計典範,必然都必須面對傳值的問題。傳值的時候,也可以使用不同的設計方式。不過,這裡我們並不在乎一個Java程式員應該自己要知道在傳值時的各種特性,我們在乎的是「有個什麼東西被傳進、傳出了」。
至於是什麼東西?在Java中,原生型別(generic types)如int、double…等或任何的物件類別也都可以,如單一個元素,或含有多個元素的陣列…等,陣列可以是原生型別,也可以是任意類別的物件。所以基本上,可以任意地傳遞任何我們想要傳遞的「東西」。
但這對我們來說也許還不夠,因為在傳遞任意數量的「東西」時,陣列本身經常沒有辦法很好地滿足我們對於資料的要求,如陣列大小的改變很麻煩、沒有高級的操作方法、任何陣列元素的改變都要由程式員事必躬親地完成…等。這樣子在準備要被傳遞的東西,或是自己程式要運算的資料時都很不方便,所以我們有了 Collection Framework,以用來實作出高級的資料結構,像是Stack和Queue。
此外,因為Collection這個介面所處理的對象是物件(Object),所以程式員很容易地就會在程式碼中將其它我們不想要的物件放到資料結構中。但是因為這些程式碼完全符合Collection介面的要求,所以在編譯時期並不會有任何的問題出現。只有在執行程式時(runtime)我們才會知道有臭蟲,這使得調試除錯變得困難;除了嚴謹的程式員,不然這類的問題是很難避免的。因此我們希望知道是否有適當的方式,可以讓編譯器在編譯時期就將問題偵測出,並讓程式員得以儘早修正?
而Java Community Process最終是以泛型來回應這項需求。
泛型的Java
可否指定Collection中所含的物件的類別,以避免runtime錯誤的產生?
既然已知類別,可否省卻其中型別轉換(case)的功夫?
在實作Collection介面時,我們所能夠處理的只有Object,如add(Object o)和 remove(Object o)這兩個主要的方法。這樣子雖然讓Collection介面變得一般化,但是也對程式員在設計類別時造成困擾,因為在大部份的狀況之下,放在實作出來的Collection中的物件都屬同一個類別。
所以Collection介面的問題出在於它太過於一般化了,使得程式員在編程時失去警覺性,舉例來說:
你有一個Canvas類別,專門用來繪製Point2D.Double類別的物件,你提供了一個 drawPoint(Collection c)方法,傳入Vector物件(以Vector類別實作了Collection介面來使它繪圖)。
為了讓它繪圖,我們要事先準備好一個內含有Point2D.Double物件的Vector,然後利用drawPoint(Collection c)傳入這個Canvas類別。現在由於Collection介面的一般化,在準備這個Vector時,我們可以將「任意」類別的Object物件放進去。首先,我們很自然地可以放進兩個Point2D.Double物件:
LIST 1
Collection pv = new Vector(); // (1)
Point2D.Double p0 = new Point2D.Double();
Point2D.Double p1 = new Point2D.Double(1.0d, 1.0d);
pv.add(p0);
pv.add(p1);
接下來,由於程式的複雜性,或是我們為了某種臨時的方便,也把某個不同類別的物件給放到這個Vector裡:
LIST 2
pv.add(new String("Debug Mode!"));
那麼在drawPoint(Collection c)方法中就會出現runtime錯誤。應該要注意的是:如果我們在drawPoint(Collection c)中也相對應地加入了處理的程式碼的話,這裡當然不會有問題出現。但是以這個例子和大部份的情況來說,我們並不想在這Vector中放入不是Point2D.Double類別的物件。在drawPoint(Collection c)方法中,最好只是把Vector中的物件先取出來,然後一律型別轉換為 Point2D.Double ,像是:
LIST 3
void drawPoint(Collection c)
{ Iterator i = c.iterator(); // (2)
while(i.hasNext()) // (3)
{ Point2D.Double p = (Point2D.Double) i.next(); // (4)
...
}
}
更動Collection Framework看來並不可行,因為真要做起來的話會沒完沒了。因此我們設想是否可以採取別的手段,把這個runtime可能出錯的問題,提前到編譯時就能解決。作法上則是引入一個新介面java.lang.Iterable<T>,並將原來java.util.Collection和java.util.List等介面設為它的子介面(在原來的階層中,Collection是根介面)。其想法如下:
我們認定某個Collection只會含有某種類別的物件,我們告訴編譯器這項資訊。在編譯時,如果程式碼中有把某個非該類別的物件放入這個Collection時,編譯時期錯誤會發生,從而避開runtime錯誤的發生。另外一方面,如果編譯器就是知道Collection中所含物件的類別的話,那麼是不是可以不要做第(4)行部份程式碼的型別轉換了呢?在這裡我們引入了一個「泛型」的新語法,並把第(1)行和LIST 3分別改寫如下:
Collection<Point2D.Double> pv = new Vector<Point2D.Double>(); //(1)
LIST 4
void drawPoint(Collection<Point2D.Double> c)
{ Iterator<Point2D.Double> i = c.iterator();
while(i.hasNext())
{ Point2D.Double p = i.next();
...
}
}
注意到它的擺放位置就可以理解到它作為「Collection 內含物件的型別說明」的意義了。要注意的是,Java的泛型僅止於提供「編輯時期的型別檢查機制」和相對應的「免除型別轉換」,和 C++ 的Template不一樣,它並不因此而產生新的類別。
改良的for迴圈
在LIST 3中我們看到了Iterator的使用,其中第(2),(3),(4)行中分別出現一次,即使在while迴圈中也出現了兩次。如果沒有要特別地取出某一個元素的話,使用Iterator好像沒有意義。既然我們就是要把Collection中的物件給取出來,有沒有辦法不必這樣子複雜地聲明也可以呢?
首先,我們理解到for迴圈可以用來實作出任何類型的迴圈,因此就以它來作為改良重覆作業(iteration)的工具。把 LIST 3 改寫如下:
LIST 5
void drawPoint(Collection c)
{ for(Iterator i = c.iterator(); i.hasNext(); ) // (5)
{ Point2D.Double p = (Point2D.Double) i.next(); // (6)
...
}
}
接下來我們把(5),(6)行的for迴圈改寫為
{ for(Object o : c) // (5)
{ Point2D.Double p = (Point2D.Double) o; // (6)
意義可以理解為就傳入的Collection c中,每次取出一個它所含有的Object 物件,並令其名稱為o。其語法表示為,
for (FormalParameter : Expression) Statement
其中Expression必須是前述的新介面java.lang.Iterable<T>的實體。搭配起 Java泛型機制(參考 LIST 4),我們可以再進一步把LIST 5改寫如下:
void drawPoint(Collection<Point2D.Double> c)
{ for(Point2D.Double o : c)
{ // Point2D.Double p = o; // (7)
...
}
}
其中第(7)行根本沒有必要了,因為這時候我們可以直接以o全取代原來p所扮演的角色,所以直接把它給註解化。程式碼至此只提供了完成迴圈工作所需要的資訊,清楚多了。考慮到巢狀迴圈的使用時,它的好處可以更加突顯出來。我們可以把巢狀迴圈寫成如LIST 7,
LIST 7【範例取自2】
List suits = ...;
List ranks = ...;
List sortedDeck = new ArrayList();
for(Suit suit : suits)
{ for(Rank rank : ranks)
{ sortedDeck.add(new Card(suit, rank)); // (8)
}
}
而按照原來的作法,迴圈的部份則必須寫成如LIST 8,
LIST 8【範例取自2】
for(Iterator i = suits.iterator(); i.hasNext(); )
{ Suit suit = (Suit) i.next(); // (9)
for(Iterator j = ranks.iterator(); j.hasNext();
{ sortedDeck.add(new Card(suit, j.next())); // (10)
}
}
自行比較一下(8)、(9)和(10)行的差異。好了,現在程式碼可以寫得這麼合邏輯,可是如果處理的是陣列,而不是Collection的話,怎麼辦?所幸這一版for迴圈的改良也考慮到了,迴圈的使用方式因此和Collection是一致的,如LIST 9:
LIST 9
void showValue(double[] v)
{ for(double d : v)
{ System.out.println("Current Value = " + d);
}
}
自動裝盒/開盒(Autoboxing/Unboxing)
目前,在for迴圈裡,不管是陣列或是Collection都可以正確地使用了。不過,如果我們有必要將像是int的原生型別放到Collection中的話要怎麼辦呢?以同樣是Collection Framework中的Map來說,如果我們想要將表示長度的String 物件(單位為公分)和其標準數值(單位為公尺;int不是物件,只好用Integer來代替)映對在一起,作法如下:
LIST 10
void setMap(String[] l)
{ Map map = new HashMap(); // (11)
for(int i=0; i < l.length; i++) // (12)
{ Integer len = new Integer(l[i]); // 使用 Integer(String s) 這個建構子
map.put(l[i], (len == null ? 0 : new Integer(len.intValue() / 100))); // (13)
}
}
因為放入map的Integer物件以公尺為單位,所以要將數值除以100。接下來在(12)行,我們以改良的for迴圈處理,而在(13)行,我們則直接將len這個Integer物件當作int原生型別來處理,這裡就是稱為「自動裝盒/開盒」的部份,改寫如下,
LIST 11
void setMap(String[] l)
{ Map<String, Integer> map = new HashMap<String, Integer>(); // (14)
for(String sl : l)
{ Integer len = new Integer(sl);
map.put(sl, (len == null ? 0 : (len/100))); // (15)
// len += 1; // (16)
}
}
說的也是,既然在(14)行中泛型已經指定了型別,為什麼編譯器不能自動幫我們處理「裝盒/開盒」?以這個例子來說明,先轉換回int原生型別,處理完後,再轉換回Integer物件的問題呢?現在,只要是在使用了泛型的Collection 中,我們就可以自動地把它其中所內含的「與原生型別相對應的物件」,視為原生型別般地加以操作,像是(15)行中的
“len/100”。
注意到len是一個Integer物件,但卻被當成int般地加以操作,原因就是 Collecion使用了泛型,但這是怎麼做到的?
前面我們提到了引入一個新介面java.lang.Iterable<T>作為java.util.Collection的父介面,除此之外,java.util.Collection和java.util.List等介面所能處理的也不再只限定在Object了,而是所謂的Element。Element是一個型別變數,也就是 java.lang.Iterable<T>中的T,這使得Java語言中像是int,float…等,不是物件的原生型別也可以在Collection Framework中進行處理。簡單地來說,就是編譯器幫你把必要的「原生型別與其相對應的物件」之間的轉換給完成,因此達到了自動裝盒/開盒的目標,並且省卻了程式員在這方面的編程功夫。
不要搞錯了,如果你不是在Collection中處理的話,自動裝盒/開盒是沒有用的。所以(16)行是沒有用的,len在一般的敘述句中還是一個物件,所以這行給註解化了。
列舉型別(enum)
列舉非常適合使用在某些狀況的表現,如季節有四季、一週的日子有七種…等。在這些狀況下,每一個變數的值可能是所列舉出來的某一個,案例如下:
enum Day { SUN, MON, TUE, WED, THR, FRI, SAT } // (17)
先來看看引入enum的第一個好處,它可以和泛型以及改良的for迴圈很好地搭配:
LIST 12
System.out.println("For Days, we have: ");
for(Day day : Day.values()) // (18)
{ System.out.println(day);
}
注意(18)行所顯示的作法。第二個好處則是,它可以使用在switch控制中。先改寫(17)行如下:
LIST 13
public enum Day
{ SUN(0), MON(1), TUE(2), WED(3), THR(4), FRI(5), SAT(6)
private final int value;
Day(int d) { this.value = d; } // Day 這個 enum 的建構子
public int today() { return value; } // 提供一個傳回代表今天的數字的int
}
然後,舉例來說,我們實作一個提示每天行程的方法,
LIST 14
void reportSchedule(Day day) // (19)
{ switch(day)
{ case SUN: System.out.println("Today is " + day.today() + "days after Sunday.");
System.out.println("HOLIDAY, Nothing to do!");
break;
case MON: System.out.println("Today is " + day.today() + "days after Sunday.");
System.out.println("Monday, prepare a paper to report.");
break;
...
case SAT: System.out.println("Today is " + day.today() + "days after Sunday.");
System.out.println("HOLIDAY again, Nothing to do!");
break;
}
}
注意到LIST 13中public enum Day的宣告、變數初始,和傳回代表今天數字的方法,還有LIST 14中(19)行傳入的enum Day,以及在switch中如何使用和呼叫public int today()方法的部份。
這樣一個列舉和switch之間的搭配,使得switch流程控制在往後的使用率應該可以大大地增加。早先,在類似狀況下想要使用switch時,我們也同時想到要提供給相對應的int值,常常最後就以if來改寫了。雖然以if改寫感覺醜醜的,但也是沒有辦法中的辦法。現在既然已經提供了enum型別,程式員應該可以少一點抱怨了吧。
最後,問個小問題,enum是原生型別還是類別?還是一種新的混血兒?你可以發覺出它和其它的不同嗎?
結論
Java這次的大改版主要的訴求在於使編寫程式更容易,並且引入了新的語法和觀念。我們可以看到,主要的變動在於引入泛型,以及環繞在它週圍的新語法(也就是 <,> 的使用)、改良的迴圈控制(for的新增用法)、自動化不言自明的型別轉換(自動裝盒/開盒)、和新的列舉型別(enum)。這些改進都是由於泛型的引入才成為可能,並且成為 Java 語言演進的動力。
如果想要對Java語言的未來發展提供建言,Java Community Process(JCP)也許是你該去參與的地方。你應該可以發現,除了新的東西被引入之外,舊的東西也由於和新東西之間良好的搭配而使 Java 出現了新面貌喔!
<作者聯絡方式:chliu@gnu.org>
|
|
J2SE 5.0一口氣新增15項JSR,內含多達十幾種的功能及語法更新。到底有哪些的更新與便利性呢?讓我們一探就竟,看看這隻老虎的威力。相關介紹請見「新版J2SE
5.0一探究竟 」一文。 |
|
針對JAVA語言應用分析舉例說明。你可在「JAVA程式語言應用」一文中得到進一步的介紹。 |
|
依項將J2SE 5.0新增功能與其功能特色詳述介紹。你可在「JAVA程式語言應用」一文中得到進一步的介紹。 |
|
號稱是昇陽在Java語言上所作最大升級的J2SE 5.0,綜合微軟開發快速,以及J2EE開發安全與穩定的優點,藉由標準化開發管理工具的介面,精簡開發撰寫語言的工夫,使得Java開發也能適應小型專案快速的需求。你可在「Java開發走向簡易化」一文中得到進一步的介紹。 |
|
昇陽公司針對J2SE 5.0新增功能與特色依其特性簡述介紹。在「 J2SE
5.0語言用法簡述」一文為你做了相關的評析。 |
|
|
|