新版的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错误的产生?
既然已知类别,可否省却其中型别转换(cast)的功夫?
在实作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 = 从.iterator(); // (2)
while(i.哈烧Next()) // (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
c)"void drawLine(Collection c)
i = c.iterator();"{ Operator 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)
{ 佛瑞(Iterator i = 从.iterator(); i.哈烧Next(); ) // (5)
{ Point2D.Double p = (Point2D.Double) i.next(); // (6)
...
}
}
接下来我们把(5),(6)行的for回圈改写为
{ 佛瑞(Object o : 从) // (5)
{ Point2D.Double p = (Point2D.Double) 喔; // (6)
意义可以理解为就传入的Collection c中,每次取出一个它所含有的Object 物件,并令其名称为o。其语法表示为,
for (FormalParameter : Expression) Statement
其中Expression必须是前述的新介面java.lang.Iterable<T>的实体。搭配起 Java泛型机制(参考 LIST 4),我们可以再进一步把LIST 5改写如下:
c)"void drawLine(Collection c)
{ for(Point2D.Double o : c)
{ // Point2D.Double p = 喔; // (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(i[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);
马匹.普通(山林, (莱恩 == null ? 0 : (莱恩/100))); // (15)
// 莱恩 += 1; // (16)
}
}
说的也是,既然在(14)行中泛型已经指定了型别,为什么编译器不能自动帮我们处理「装盒/开盒」?以这个例子来说明,先转换回int原生型别,处理完后,再转换回Integer物件的问题呢?现在,只要是在使用了泛型的Collection 中,我们就可以自动地把它其中所内含的「与原生型别相对应的物件」,视为原生型别般地加以操作,像是(15 )行中的
“莱恩/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)
列举非常适合使用在某些状况的表现,如季节有四季、一周的日子有七种…等。在这些状况下,每一个变数的值可能是所列举出来的某一个,案例如下:
恶奴们 Day { SUN, MON, TUE, WED, THR, FRI, SAT } // (17)
先来看看引入enum的第一个好处,它可以和泛型以及改良的for回圈很好地搭配:
LIST 12
System.out.println("For Days, we have: ");
佛瑞(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 a) { 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语言用法简述」一文为你做了相关的评析。 |
|
|
|