ccic134302
发贴: 185
积分: 40
|
于 2003-05-07 10:06
本章介绍“Java豆”的概念、相关类库和开发的基本方法。
“Java豆”是Java技术的一个发展。Java语言的设计者们于1996年底推出了Java豆应用程序接口的正式说明书。引入Java豆的目的是为Java提供一个新的软件组件(Component)模型,以便使用Java开发的软件可以很方便地被重复使用,或组装起来以适应新的需要。简单地说,意图是让开发者开发出来的程序(豆)相对独立,具有统一的接口,如同积木一般可以装配。尽管这一概念尚不完善,但已引起许多人士的注意。“Java豆”的发展将为软件复用提供一种新的解决方案。
本章的内容与前面各章相对独立。不仅如此,在系统软件方面也有较高的要求。实验Java豆要求JDK版本在1.1版本以上,并至少拥有BDK1.0版本。BDK(Beans Development Kit)是一个与JDK配合使用的工具包,它能演示“豆”被操作和使用的过程,并支持简单的Java豆开发。BDK可以从“ftp://splash.javasoft.com/pub”下载。
学习完本章以后,读者应对“Java豆”有较全面的了解,熟悉BDK的使用和JDK1.1版本中与Java豆相关的改进,并能开发简单的Java豆。
15.1 什么是Java豆
这一节我们先对Java豆建立一个初步的感性认识,然后阐述其概念。
15.1.1 BDK的使用——认识Java豆
我们先把BDK运行起来。假设你拥有JDK1.1.1版本和BDK1.0版本,分别安装在c:\JDK1.1.1和c:\BDK目录下。运行你的BDK的操作步骤如下:
(1)打开一个命令提示窗口,进入BDK安装目录的beanbox子目录。
(2)设置路径为JDK1.1.1版本的bin目录,如\JDK1.1.1\bin。
(3)在命令提示符下键入“run”并回车。
“run”批命令陆续被执行,最后,屏幕上出现三个窗口(见图15.1),标题栏分别为“ToolBox”,“BeanBox”和“PropertySheet”。这三个窗口的出现可能要花费稍长的时间。只要不出现异常(Exception),请耐心等待。
三个窗口打开后,BDK已进入了工作状态。ToolBox窗口中有16个项,代表了BDK中附带的16个Java豆示例。可以看到有的豆带有图标。PropertySheet窗口用于编辑豆的属性(主要是外观属性)。中间的大窗口BeanBox是用来装配和编辑豆的,它有两个菜单项,完成对豆的存取、编辑操作。
下面我们用BDK附带的示例Java豆做几个小实验,以验证BDK的功能。
实验1 关联属性
关联属性是豆属性的一种,基本的含义是将两个豆的若干同一类型的属性关联起来,使一个豆的属性发生改变时,另一个相应属性也随之改变。15.3.2将予以详细介绍。这个实验将演示BDK的基本操作,并把示例豆“TransitionalBean”和“JellyBean”的属性“color”关联起来。
点击BeanBox的Edit菜单项,看到五个项。这是对于当前对象(当前是Beanbox)可以施行的操作,往Beanbox中放入一个“豆”,菜单项可能发生改变。按下面步骤操作:
(1)以鼠标点击ToolBox中的“TransitionalBean”。光标变为十字型。
(2)在BeanBox的中央偏上处点击一下,出现一个用桔黄色填充的圆角矩形。它周围带有短粗斜线组成的方框,表示它是当前被选中的对象。
(3)观察PropertySheet窗口。其中的内容发生了变化,它包含着TransitionBean可以直接修改的属性。看看Edit菜单项的下拉菜单,无变化。
(4)用鼠标点击ToolBox中的“JellyBean”。
(5)在BeanBox中,TransitionalBean的下方点击一下,出现与(2)中相同的图案。
观察PropertySheet窗口,与(3)中相比,多了一些属性,可见两个豆外观虽相似,功能却不同。
(7)点击Edit菜单项,发现多了一项“Bind Property...”点击之,出现属性名对话框(“PropertyNameDialog”)。点中“color”,并点“OK”键。
把光标在BeanBox中移动,可见它牵引着一条从JellyBean发出的射线。点击TransitionalBean,又出现属性名对话框。点中“color”,并点击“OK”键。
(9)点击JellyBean,使它被短粗斜线组成的矩形框包围,即被选中。
(10)在PropertySheet窗口中的“color”后的桔黄色矩形上点击,出现颜色编辑器对话框(“sun.beans.editors.ColorEditor”)。其右部的下拉列表框中显示“orange”,将其值改为“red”。可以看到,两个圆角矩形都变成了红色。
(11)再改变几次颜色,可以发现,改变JellyBean的颜色将导致同时改变TransitionalBean的颜色,反之亦然。
上面的例子演示了两个豆的“关联属性”是如何被BDK关联的。应注意,BDK只是一个演示性的工具包,仅仅支持初步的开发,其目的与其说是作为开发工具不如说是作为正式的(将来的)开发集成环境做功能上的示意。因此,它的界面较为粗糙,运行也不稳定。但目前BDK仍是帮助我们了解Java豆的必不可少的工具。
实验2 几个豆的协同工作。
在这个实验中将演示如何为几个豆建立相互之间的事件处理关系,使得某个豆可以引起别的豆的反应。这实际上就是把若干豆装配到一起以协同完成一项工作的过程。
用“File”菜单的“Clear”选项清除整个BeanBox窗口的内容,进入第二个实验。
(1)点击Toolbox中的“Juggler”项。
(2)在BeanBox的中央点击一下,之所以在中央是因为该豆的图像面积较大。如果位置不合适,造成图像不能完全显示也不要紧,可以像移动一个窗口一样,用鼠标点在短粗斜线组成的边框上拖曳以移动图像。可以看到BeanBox中间出现Juggler的动画。
(3)点击“ToolBox”中的“ExplicitButton”,放在BeanBox中的空白处。相应位置将出现一个按钮,上面写着“Press”。在选中该按钮的情况下,在PropertySheet中找到“label”栏,将其中的“press”改成“Start”。
(4)点击ToolBox中的“OurButton”,置于BeanBox中。类似(3),将按钮上的文字改为“Stop”。
(5)分别选中“Start”“Stop”按钮,比较它们的PropertySheet窗口内窗口有什么不同,Edit子菜单的内容有何不同,并比较Edit子菜单的Events项引出的次级子菜单。
选中“Start”按钮,点击Edit菜单项的Events子菜单中的button push项,直到出现“actionPerformed”项,点击之。可以看到自Start按钮引出一条射线。点击Juggler豆,出现事件目标对话框(“EventTargetDialog”)。
(7)选中“startJuggling”项,点击“OK”,等待对话框自动消失。
此时BeanBox中的Juggler豆处于被选中状态。用鼠标点击“Stop”按钮,使它被选中。在Edit菜单中,逐级点击Events|action|actionPerformed,可见从“Stop”按钮引出一条射线。
(9)点击Juggler豆,再次出现事件目标对话框,选择“stopJuggling”项并按“OK”。等待对话框自动消失。
(10)按动Stop按钮,Juggler豆的动画就静止了,按Start键则动画重新开始。
(11)选中Juggler豆,将PropertySheet窗口上的AnimationRate(动画速率)的值由125改为500。可以看到,按动Start后,动画速率比原来明显变慢。
在实验2中,我们把两个按钮与Juggler豆关联起来。我们用到的Star键是ExplicitButton的一个实例,而Stop键是OurButton类的实例,ExplicitButton系从OurButton派生而来。可以看到,Juggler豆对两个按钮的事件作出了响应。
以上两个实验是为了给读者一个感性认识。具体的原理(如事件传递、属性窗口的不同、编辑菜单项为何不同)将在本章的后续各节分别介绍。读者还可看BDK的有关文档,自己组装编辑Java豆。
当一个豆被选中时,它可以被移动,有的豆还可以改变大小。选中一个豆的方法就是点击它。但有时可以发现点击一个豆似乎不起作用,这时你可以点击它的边缘,一般就可以达到目的了。
15.1.2 什么是豆
Java豆应用程序接口说明中,为“豆”给出了这样的定义:“Java豆是一种可重用的软件组件,它可以在创建工具中被可视化地操纵”。
如果有过使用Visual Basic,VisualAge或Delphi的经验,读者将不难理解这一定义。不过,即使没有接触过类似的东西也不要紧,我们可以依据15.1.1中的实验对这一定义做一些说明。
上一节的实验2中,我们使用了三个豆,做了一个小小的动画演示。现在我们倒过来看这个问题,假如要演示动画,我们应做什么?显然需要有显示区域和命令输入。于是我们选用了三个豆。以ExplicitButton为例,它被开发并非为我们这个演示,也不仅仅用于这个例子中;它可以用在一切适宜的需要一个按钮的地方。这个按钮就是一个“可重用的软件组件”,即软件的一个相对独立的组成部分,并可以被重复使用。
许多开发环境允许程序员进行可视化操作,往往它们提供一个叫做“form”或“window”或其它名字的“白板”,开发人员可以把自己所需要的部件(如滚动条、按钮、下拉列表框等)用鼠标拖放在上面,组成一个整体。虽然这些开发环境对于开发带有图形用户界面的应用尤为适合,但并不局限于可视的应用。这些开发环境就是所谓的“创建工具”。BDK虽然比较简单和粗糙,但仍然称得上是一个创建工具。当然,为了开发实用的“豆”,我们还需要更多的功能,如文本编辑、图形编辑等等。
至于可视化操纵,经过15.1.1中用鼠标操纵了若干个豆后,它的意义对读者来说是很明显的。Java豆就是这样的一种组件。应该说,可重用的软件这一思想久已有之,虽然形式不同,基本思想是一样的。它们往往要求统一的标准接口,并提供相应的机制(参见15.4“自省”)让创建工具了解它的性质、行为,从而使开发人员能够运用它们、组装它们。Java豆也在寻求与已有的软件组件(如微软的Active X)相联接的方法,并已取得一定成效。
Java豆具有如下的典型特征,正是这些特征保证了它的功能:
(1)“自省”功能(Introspection),即提供相应的机制,使创建工具能够分析它的行为和性质。
(2)可以定制(Customize)。即某些属性(properties)可以通过创建工具来修改。如15.1.1中,我们用BDK的PropertySheet窗口修改了ExplicitBtton的标签。
(3)可以被组合。这是通过事件机制来完成的(详见15.2)。
(4)可以存储、装入。这使得对豆编辑和联接的结果可以被录入保存,并在适当的时候取出来应用。BDK的File菜单中有存储和装载豆的菜单项,读者不妨一试。
Java豆的三大要素是属性(Properties)、允许其它组件调用的方法集和能够激发的事件集。
属性值可以通过调用相应的方法来修改或获取。如用getLabel()获取标签值,setLabel()设置标签值等。
public方法一般来说都可以被其他组件调用,但豆可以限定只有部分public方法可以被调用。
事件是组件之间联系的手段,JDK1.1版本和BDK中引入了一种有别于过去的事件处理的模式,即代理(Delegation)模式。在这种模式中,存在事件源与事件监听者。二者通过“登记”联系起来,一旦事件源处发生了相应的事件,对应的监听者的方法就被调用来处理它。
以上对Java豆做了概要的介绍。后面几节中,我们先分别介绍Java豆应用程序接口的各方面的规则,然后结合实例介绍Java豆的开发。
15.2 事件机制
作为一个组件,其自身的事件处理以及与其他组件的交互无疑是一个重点。一个豆要实现它的功能,事件处理是核心。本节介绍Java豆的事件机制。
15.2.1 Java豆的事件机制
Java豆的事件机制,就是把对象的状态改变通知给其它相关对象以进行适当处理的机制。当一个对象激发了某个事件(如鼠标点击、键盘操作)时,如何做出响应呢?这一节我们来讨论这一过程的细节。
⒈基本模型
假如有对象a为类A的实例,对象b为类B的实例。程序的功能要求,当a激发了一个事件e时,b要作出相应的反应。因而就有下面的过程(如图15.4)。
图15.4 事件模型㈠
上面这一过程看起来很自然,但存在两个问题:(1)当事件e发生时,对象a如何知道它将把事件给谁?(2)对象a可能激发的事件假如有很多,如何安排响应者(监听者b)?如果对每个事件都对应一个监听者,显然对象数目就太多了。
对上面两个问题,Java豆的解决方案是这样的:
(1)每一类事件由一个方法来处理,而同族的事件被组织成一个监听者接口。实现这一接口的类便可处理对应的事件,其对象就是监听者对象。
例如,鼠标点击(Click)和双击(Double Click)是两类事件,由于发生原因近似,我们可以定义一个接口来组织它们,形如:
public interface MouseActionListener extends java.util.EventListener{ public void mouseClick(MouseActionEvent e); public void mouseDoubleClick(MouseActionEvent e); }
(2)事件的有关信息也需封装起来成为类,传递给监听者,以便响应事件时使用。这种事件类是java.util.EventObject的子类。就像异常(Evception)类的各子类一样,有的事件类之所以被创建仅是为提供不同种类的事件状态的逻辑划分,而未必具有独有的数据。换句话说,有的事件仅提供一个形式。如上面接口中的MouseActionEvent可以这样定义:
public class MouseActionEvent extends java.util.EventObject{ public MouseActionEvent(java.awt.Component src){ super(src); } }
如上这种封装事件有关信息的类称为事件状态类(EventState Class),其实例为事件状态对象。
由此我们可以定义一个监听者类B,监听鼠标点击/双击事件:
Class B implements MouseActionsListener{ public void mouseClick(MouseActionEvent e){ //...处理点击事件 } public void mouseDoubleClick(MouseActionEvent e){ //...处理双击事件 } //...类中其它数据和方法 }
至此,前面的第二个问题已经解决了。也就是说,不必对每一类事件对应一个监听者,而是用接口把同族的事件分组,而以实现该接口的类作为几类同族事件的共同监听者。
但第一个问题仍然是悬而未决:如果有超过一个的对象都发生了鼠标点击/双击事件,它们如何把事件递给真正相关的监听者呢?这就要用到“监听者登记”技术了。
Java豆的应用程序接口说明中指出:“为使潜在的诸事件监听者能与正确的事件源实例登记,以便建立从源到监听者的事件流,事件源类应提供登记与注销的方法。”
这就是说,事件源对象应包含登记与注销监听者的方法,执行登记方法后,就将源与监听者联系了起来 ;待这种联系不再必要时,还可以注销掉。仍以前面的例子为例,作为事件源的A必须有这样的方法:
class A{ public void addMouseActionListener(MouseActionListener l){ //实现细节 } public void removeMouseActionListener(MouseActionListener l){ //...实现细节 } //...其它成员数据和方法 }
然后,需要建立对象a、b之间的关系时,用
a.addMouseActionListener;
就可以完成登记过程。而当特定时刻后不再需要b来监听a的事件时,又可用
a.removeMouseActionListener;
来注销前面的登记。
综合上述关于监听者、事件源、事件状态关系,我们可以得到下面这张联动关系图(图15.5)。为了节省篇幅,类A的定义中只列出了登记监听者方法,类B的定义中也只列出了一个方法。
图15.5 事件模型㈡
上图中需要解释的是如何决定b应调用哪个方法来响应事件。这个工作不是由b来完成的,而是由a。事实上是由事件源对象分辨同族事件的种类,并调用监听者的相应方法的。15.2.2之2将给出这一过程的实现。
由上图进行抽象可以得到基本事件模型的图示(图15.6)。
图15.6 事件模型㈢
总而言之,Java豆的基本事件模型有以下几个要点:
(1)激发事件的对象,即事件源对象,它们的定义中包括特定的登记监听者和注销监听者的方法,以便与相应的监听者对象相联系。
(2)每一类事件通知(notification)被定义成方法,相近的方法由事件监听者接口分别组织起来。事件监听者接口由java.util.EventListener继承而来。
(3)事件监听者类通过实现某些接口来确定它们对何种事件感兴趣。
(4)与某事件通知有关的信息被封装于事件状态对象中,该对象的类由java.util.EventObject继承而来,作为一个唯一的参数传递给事件方法。
(5)对事件的响应是由监听者对象的方法调用实现的。
读者可根据这几点对照前面的例子,以加深对基本事件模型的理解。
⒉编程中的命名规则
Java豆是可以在应用创建工具被可视化操纵的组件,这就要求创建工具对豆的性质和行为有充分的了解。以节15.1.1的例2为例,第、(9)步中,我们将OurButton对象的actionPerformed()方法与Juggler对象的stopJuggling()方法联系起来,其中的内在联系是:
(1)OurButton类定义了addActionListener(ActionListener l)方法。
(2)ActionListener接口中定义了方法actionPerformed(ActionEvent e)。
(3)Juggler类中有方法stopJuggling(ActionEvent e)。
创建工具BDK就是分析出了各类所具有的相应方法,从而能够将这两个豆组装在一起。
为了使创建工具能够机械地识别豆的性质和行为,我们编程时要遵循一定的命名规则。不仅在事件模型中,而且在属性的修改、关联时都要遵循一定的规则。将在涉及到这些内容时将分别予以介绍。
对于基本事件模型,命名的规则是:
(1)事件状态类往往以“**Event”命名,“**”表示任意字符串,如MouseActionEvent,KeyEvent等。
(2)监听者接口往往以“**Listener”命名,如“ActionListener”。
(3)事件源类中的登记和注销监听者的方法按如下定义:
public void add<监听者接口名>(<监听者接口名>listener);
public void remove<监听者接口名>(<监听者接口名>listener);
如addActionListener(ActionListener l)暗示着存在一个监听者接口ActionListener。
注意,为了防止多线程代码中的竞争,登记、注销监听者的方法常常是同步的。也就是说,要加上“synchronized”修饰符。
(4)创建工具在事件发生后要将事件传递给监听者。因而,监听者处理事件的方法(即监听者接口中定义的方法)一般要以该事件类的实例作为参数,哪怕这一事件的信息在作出响应时可能并不需要。
(5)为事件源编写登记、注销的方法时,优先利用该事件源类自父类继承来的方法(如java.awt.Component类有addFocusListener()方法),尽量不另外创建新的方法。这样所编写的代码易读、标准,也省去设计接口的麻烦。当然,如果现有的方法和监听者接口不能满足需要,就必须另外编写了。
⒊事件模型的扩展
(Ⅰ)适配器(Adapter)
基本事件模型说明了事件源与监听者之间的联动关系,然而,仅有这个基本模型是不够的。当事件的传递和响应中需要一些附加的工作,或者按基本模型不能编写出适合要求的监听者时,需要引入适配器对象。
考虑节15.1.1的例2,要实现按钮与Juggler的控制关系,如以基本模型来考虑,就至少应有下面的条件:
(1)按钮OurButton的类中有addActionListener()方法;
(2)Juggler类实现了ActionListener接口。
这样,当按钮被点击,OurButton就激活了一个ActionEvent事件。如果Juggler类实现了ActionListener接口,就会执行它的actionPerformed()方法(actionPerformed()是ActionListener接口包含的唯一方法),从而实现响应。然而,Juggler是一个已编写好的类,而且没有实现actionListener接口。那么这两个豆是如何联系起来的呢?
这个问题并不难解决。假如我们是要把两个豆在手写的程序中组合起来,那么我们可以手工编写一个中介类。它实现ActionListener接口,而在方法actionPerformed()的实现中,调用Juggler的适当方法,比如,对这个例子可以编写:
Class MyAdapter implements ActionListener{ //...其它信息 public void actionPerformed(ActionEvent e){ juggler.startJuggling; } }
其中,juggler是Juggler类的对象。而事件源要登记监听者时,应登记:
OurButton btn=new OurButton();
btn.addActionListener(new MyAdapter());
这样,即可完成两个豆的联动。
这里的中介类MyAdapter就是一个“适配器”类。顾名思义,适配器就是充当桥梁和纽带作用的。从功能上说,适配器并不是真正的监听者,它只是在事件源与监听者之间搭了一座桥。真正的监听者类并未实现监听者接口,而是经过适配器的传递作出反应。以上例为例,Juggler对象是真正的监听者对象,而MyAdapter类的实例是适配器对象。
加入适配器后的事件模型示意图如下(图15.7)
图15.7 含适配器的事件模型
事实上,BDK和应用创建工具也往往采用标准的适配器来完成豆的联接。如我们在BDK中组合OurButton和Juggler豆时,OurButton类激发的事件是ActionEvent对象,则缺省适配器就会实现ActionListener接口,以提供恰当的中介。
适配器是Java豆事件模型中极端重要的一方面。适配器可以针对一族事件,也可以针对多族事件,前者如前面的例子,后者也很简单,只要使适配器类实现多个监听者接口就可以了。
(Ⅱ)有任意参数表的事件方法
前面提到的各种监听者接口中,无论定义何种方法(即事件方法),其参数都是唯一的,即参数是一个事件对象,它的类是java.util.EventObject的子类。
在特殊情况下,允许放宽这一限制,即事件方法可以有有限多个参数。比如与其它语言接口时,或者某些实现场合要求方法必须有多个参数时可以这样做。应用创建工具应能支持这样的事件方法。但这种不标准的事件方法应尽量避免使用,而是以标准形式与适配器的组合来实现其功能要求。
(Ⅲ)多匹配与单一匹配(Muliticast and Unicast)
所谓多匹配与单一匹配的问题,是指对于同一类事件允许有几个监听者监听的问题。一般的事件发送是多匹配的,也就是说,对每一类事件可以有多个监听者来监听。例子是我们可以用一个按钮来控制两个Juggler对象,按动一次而使两个动画都开始。
但在有此情况下,应用将要求某一事件仅能由一个监听器来监听,这种情形与上一种情形的不同在于事件源的登记监听器方法的不同。该情形下登记监听器方法的形式为:
public void add(监听者接口名)(<监听者接口名>listener) throws java.util.TooManyListenersException
如果发生了企图登记一个以上监听者的情况,将抛出TooManyListenersException异常。
另外,如果以null作为参数调用登记方法,无论在多匹配还是单一匹配的情况下,都是不合法的,将引发IllegalArgumentException异常或NullPointerException异常。
(Ⅳ)并发控制
在多线程环境下,事件控制极易引发竞争和死锁。在必要的场合,需要使用synchronized修饰符。为了避免死锁,又要尽量用“同步块”来代替整个同步方法。如本章后面将讲到的例子中有这样一段代码:
...
Vector l; if(cmd.equals("+")){ setValue(getValue()+1); ActionEvent e = new ActionEvent(this,0,null); synchronized(this){ l=(vector)listeners.clone();//同步块 } ...
这就是以同步块而非同步方法实现事件控制的过程。
15.2.2 编程实现
节15.2.1中,给出了有关方法的原型而未给出具体的实现。这一节讨论事件机制的程序实现。
1.监听者的登记和注销
事件监听者一旦登记,一般是存入一个向量(Vector)。但对于关联属性与受限属性(节15.3)的改变事件,java.beans类库中另提供了两个类PropertyChangeSupport与VetoableChangeSupport来存储登记的监听者。对于一般的监听者,登记时只要将它放入一个向量即可。如对于节15.2.1提到的例子可以这样实现:
public class A{ public synchronized void addMouseActionListener(MouseActionListener l) listeners.addElement(l); } public synchronized void removeMouseActionListener(MouseActionListener l){ listeners.removeElement(l); } //...其它方法 private Vector listeners = new Vector();//向量,存储监听者 }
2.事件的激发与处理
前面提过,事件的分析与处理实际上是由事件源来完成的。这一过程在旧的事件模型中,在handleEvent()方法中实现;在新的事件模型中,则表现为事件源类(或适配器)实现若干事件监听者接口(如WindowListener,MouseListener),在该接口的方法中做分析处理事件的工作。但无论在哪种情况下,要做的工作都是一样的,大致有:
(1)复制一个存储监听者的向量;
(2)创建事件状态对象,以备传递;
(3)对已登记的所有事件监听者,分别调用它们的事件处理方法。
仍以前面有的例子为例,对鼠标点击事件的处理为:
... Vector l;//声明一个向量,以备复制监听者存储向量 ...//判断事件鼠标为点击事件,具体过程略 MouseActionEvent l = new MouseActionEvent(this); //创建一个事件,以备传递,MouseActionEvent的定义见(节15.2.1之1) synchronized(this){ l=(Vector)listeners.clone();//复制一个存储监听者的向量 //复制向量 listeners时之所以采用同步块是为了避免多线程方式下对listeners的读写导致 //错误。为了减少死锁的发生,这里采用同步块,而不是把该方法作成同步方法。 for (int i=0;i<l.size();i++){ MouseActionListener mal=(MouseActionListener)l.elementAt; mal.mouseClick; }//对登记的若干个MouseActionListener对象分别进行处理,调用其处理该事件的方法 //mouseClick()。 ...
以上是分析处理事件的模式。可见,“传递事件”是指调用监听者适配器的方法而以事件作为参数。
在节15.6中给出的两个Java豆的完整例子中,读者可以看到上述实现细节的具体使用。
本节介绍了Java豆的事件机制。这是实现应用功能的核心部分,希望读者认真领会,并参照BDK提供的其它例子学习。
15.3 豆的属性(Properties)
属性可以影响豆的外观和行为。比如,一个可视的豆的背景色、前景色都是属性。节15.1例子中,Juggler豆的动画速率(animationRate)是属性;BDK附带的JDBC SELECT 豆的“URL”也是属性。每当你在BDK中选中一个豆,都可以在PropertySheet窗口中看到其可修改的属性。
Java豆的属性可以是各种类型的,或是各种类的实例。要访问一个属性,需要调用它所在类的访问方法(access method)。这些方法可分为读(get)和写(set)两类。为了BDK及其它创建工具能够自动分析出豆所具的属性,访问方法也有固定的命名规则。
即使在BDK的PropertySheet窗口中直接修改属性,属性也还是通过调用访问方法来修改。因而在开发Java豆的时候,就要编写适当的访问方法来读/写属性。对属性的写操作往往会有一些副作用,如导致豆被重画等等。
下面我们来介绍属性及其访问方法。
15.3.1 访问方法的命名规则
对于简单的属性(即指仅包含一个数据域的属性),命名规则为:
(1)读方法:<属性类型>get<属性名>()
如:Color getBackGroundColor();
(2)写方法:void set<属性名>(<属性类型>value);
如:void setBackGroundColor(Color value);
上面两个方法访问一个名字叫BackGroundColor的属性,该属性的类为Color。
对于数组型的属性(我们称之为“下标属性”,即“indexed properties”),命名规则略有不同。下标属性涉及一组值,在访问时要指明需要的具体是什么。目前,Java豆要求下标必须为整形(int)。
对下标属性的访问方法有两种,其一是对整个数组同时访问,其二是只访问单个的下标变量。命名规则分别为:
(1)读整个数组:<单个属性类型>[] get<属性名>();
如:Color[] getColors();
(2)读单个元素:<单个属性类型> get<属性名>(int index);
(3)写整个数组:void set<属性名>(<属性类型> values[]);
(4)写单个元素:void set<属性名> (int index,<属性类型> value);
这些方法在数组越界时会抛出java.lang.ArrayIndexOutOfBoundsException异常。无论对简单属性还是下标属性,访问方法都可以抛出异常,就像其它方法一样。
15.3.2 关联属性(Bound Properties)
关联属性的演示在节15.1的例一中已经出现了。引入关联属性的用意是显然的,即当一个豆属性发生改变时,可以把这个变化通知给其他对此感兴趣的豆或容纳该豆的容器,以使它们作出适当的反应。关联属性的变化不仅仅可以引发某个豆对应的某个属性发生变化。看了下面的分析读者就会知道,关联属性的变化也是激发了一个事件,而事件的监听者(或有适配器情况下的实际监听者)监听到事件变化后可以作出各自相应的反应,改变自已的对应属性仅是反应的一种而已。
类库java.beans(BDK 及JDK1.1版中均有)中,包含了与关联属性相关的接口与类。编程时使用它们就可以完成对属性改变的处理。整个作用过程也与一般的事件处理相似。不同的是,用来存储“属性改变监听者”(PropertyChangeListener)的不是一个向量,而是类库提供的PropertyChangeSupport类的对象。另外,类库还提供了PropertyChangeListener接口与PropertyChangeEvent类,分别作为监听者接口和事件状态类使用。
类似节15.2.2,关联属性的编程实现是:
(1)监听者登记/注销
public void addPropertyChangeListener(PropertyChangeListener l){ changes.addPropertyChangeListener(l); } public void removePropertyChangeListener(PropertyChangeListener l){ changes.removePropertyChangeListaer(l); } private PropertyChangeSupport changes = new PropertyChangeSupport(this);
上面这些代码一般可以不加任何修改而直接用在程序中。
(2)事件的激发和处理
由于事件是在修改属性时发生的,因此事件的创建和传递在相应属性的写方法中进行。而且,PropertyChangeSupport类已为我们提供了触发事件的方法,这使我们不必一一寻找监听者并调用其方法。具体的作法是:
public void set(<属性类型> value){ 创建一个该属性类型的变量或对象,不妨记为oldValue; oldValue = 属性值; 对属性赋以新值value; changes.firePropertyChange("<属性名>",oldValue,value); }
如果属性是基本类型的(如int),则在调用firePropertyChange时,仅用oldValue、value是不够的,需要把它包装成类对象,如:
changes.firePropertyChange("<属性名>",new Integer(oldValue),new Integer(value));
注意,先修改了属性的值,然后激发相应的属性改变事件。
对于属性事件的监听者来说,它只需实现PropertyChangeListener接口,在这个接口中仅有一个抽象方法
public abstract void propertyChange(PropertyChangeEvent evt)
在PropertyChangeEvent类中封装了激发该事件的对象(事件源对象),属性原值,属性新值,属性名等信息,可以用相应的方法读取。于是监听者就可以根据属性的变化作出相应的反应。
除了节15.1.1的例子之外,BDK中的ChangeReporter豆也是关于关联属性的。将一个豆A的propertyChange事件与ChangeReporter豆的reportChange方法联系起来,修改A的属性,ChangeReporter就会显示属性名及其新值,读者不妨自己实验一下。
15.3.3 受限属性(Constrained Properties)
某些时候,豆的一些属性是不希望被改变的。或者,是否修改需要经过其它豆或容纳该豆的容器来判断,这些属性称为受限属性。受限属性实现也很简单,即在修改属性之前激发一个受限属性的改变事件,如果不发生异常,则继续其修改工作,改变属性值;若由于其它豆的干预而产生了异常,就不进行修改。
java.beans类库中也提供了相关的类和接口。VetoableChangeListener接口是受限属性改变监听者接口,并提供激发属性改变事件的方法,这一方法可能抛出受限属性改变异常PropertyVetoException。异常类PropertyVetoException中封装了描述信息,并提供了获取相应属性改变事件的方法。
受限属性的实现与关联属性基本相同,但激发事件是在修改属性之前。
(1)属性的登记/注销
private VetoableChangeSupport vetos = new VetoableChangeSupport(this);
public void addVetoableChangeListener(VetoableChangeListener l){
vetos.addVetoableChangeListener(l);
}
public void removeVetoableChangeListener(VetoableChangeListener l){
vetos.removeVetoableChangeListener(l);
}
(2)事件激发和处理
下面的方法修改一个受限的关联属性。在修改属性值之前,先激发一个受限属性改变事件,修改之后,再激发一个关联属性改变事件。注意此方法可能抛出的异常。
public void set <属性名>(<属性类型> value) throws PropertyVetoException{
创建一个该属性类型的变量或对象oldValue;
oldValue = 属性值;
vetos.fireVetoableChange("<属性名>",oldValue,value);
对属性赋以新值value;
changes.firePropertyChange("<属性名>",oldValue,value);
}
受限属性改变事件的监听者只需要实现VetoableChangeListener接口,即实现
public abstract void vetoableChange(PropertyChangeEvent evt) throws PropertyVetoException
方法,即可处理该事件。
BDK中提供的Voter类提供对象受限属性的处理。将JellyBean的vetoableChange事件与Voter豆的VetoableChange方法相关联,并改变受限属性priceInCents,看发生的现象。改变Voter类的vetoAll属性后,再改变priceInCents,看有什么改变。这可以给读者一个感性认识。
15.4 定制(Customize)
Java豆是一种可重用的软件组件,为了达到多次重用的目的,Java豆就要有一定的通用性。因此,在实际使用时,需要对豆的外观、行为作一定的修改,使之符合需要,这一修改过程称为定制。
定制的方法有两种,第一种是用PropertySheet来完成的,这种方法我们已经很熟悉了。另一种方法是编写豆的Customizer类,用这个类来完成复杂属性的修改、行为的设定。第一种方法适用于中小规模的豆,而后一种方法较为灵活,能够处理较为复杂的情况。
为了获得一些感性认识,让我们把BDK运行起来,在BeanBox中放置一个JDBC SELECT 豆,点击Edit菜单中Customize...项,可以看到一个定制器(Customizer)窗口。
无论是定制器还是属性表单窗口,基本的组成都是若干个项,每个项对应一个属性。属性有各自不同的编辑方式。对于一些常用的属性,BDK提供了相应的编辑器(Editor)。在BeanBox中选中JDBC SELECT 豆,然后点击PropertySheet中的foreground项,可以看到一个新窗口ColorEditor。这就是BDK为Color对象提供的编辑器。如果开发豆时引入了新的类型,开发人员应附带开发相应的编辑器。
那么,如何使BDK或创建工具明白用什么编辑器来编辑某一类型的属性呢?Java豆的“属性编辑器管理器”(PropertyEditor Manager)可以完成这一工作,该类中的方法
public static void registerEditor(Class targetType,Class editorClass)
使特定类型可以与它的编辑器联系一起。
当遇到一个类型时,BDK按下列规则来搜索属性编辑器:
(1)首先寻找有无显式注册的编辑器(注:Java的基本类型如int,bookean等的编辑器已预先注册)。
(2)其次寻找给定属性类型加“Editor”构成名字的编辑器类,如,当遇到属性类为abc.cde.Fgh时,将寻找有无abc.cde.FghEditor。
(3)最后,将在给定的类搜寻表中这样的类:其名字为属性类名的最后一节加上“Editor”。所谓给定的类搜寻表是指BDK对编辑器的搜索路径,一般是java.beans.editors,故对abc.cde.Fgh将寻找java.beans.editors.FghEditor。
当一个编辑器修改了某个值时,它将激发一个PropertyChange事件,事件的接收者(如创建工具)检测到事件后,根据新值,调用相应属性的写方法完成对目标的修改。
编辑器对豆的修改应是间接的,即并非直接修改目标对象,而是创建一个新的对象,对它进行修改。当修改完毕激发了PropertyChange事件后,事件接收者将检索这一新的对象,根据它作对目标对象的修改。
属性的定制器Customizer的命名规则为<Java豆名>Customizer,它必须是java.awt.Component的子孙类,并实现java.beans.Customizer接口,它应是一个独立的对话窗口。
Customizer接口中有三个方法:addPropertyChangeListener()方法,removePropertyChangeListener()方法和setObject()方法。其中public abstract void setObject(Object bean)方法用以设置被定制的豆,该方法只能调用一次。
节15.6例2中编写了一个简单的属性定制器,它是由BeanInfo类通知给BDK的(参见节15.5)。BDK一旦得知某个豆有定制器,就会在BeanBox的Edit菜单中加一项“Customize...”。点击它,便可显示出相应的定制窗口,读者可以从节15.6例2中大致了解其实现过程。
15.5 自省(Introspection)
所谓“自省”是指创建工具了解豆具有何种属性及行为的过程。Java豆要求能在创建工具中被可视化地操纵,当然也就要求创建工具能够分析出豆的属性、行为。这个分析过程也有两条途径可走。一种是低级的“反映”(reflection)机制,另一种是较为高级的,即由豆的开发者提供一个类来提供描述豆的信息。
15.5.1 反映机制
反映机制说起来不复杂。它是依赖命名规则与类的继承关系来完成工作的。前面曾经数次提到了命名规则的问题。要求编程人员遵循标准命名规则,就是为了方便反映机制的实施。
对于每一对匹配的get<属性名>、set<属性名>方法(所谓匹配是指方法名中的“<属性名>”相同,且get方法的返值与set方法的参数为同一类型),则认为被考察豆有可读写属性,名字即<属性名>。如果仅有一个方法get<属性名>(或set<属性名>),则认为被考察豆有一个只读(只写)属性。
布尔型的属性与其它属性稍有不同,它的读方法是:
public boolean is<属性名>();
这个方法可用来取代get方法,也可以与get方法共存。
类似地,下标属性的读写方法也说明了豆具有下标属性。如,一对方法
public <属性元素> get <属性名> (int a);
public void set<属性名> (int a,<属性元素> b);
说明了一属性,其类型为数组<属性元素>[]。
2.分析事件
对于每一对add<监听者> (<监听者> l)和remove<监听者>(<监听者> l)方法,反映机制将确定存在一个事件监听者,如果add方法不抛出java.util.TooManyListenerExecption,则认为它是多匹配监听者,否则,,视作单匹配监听者。
对方法而言,认为所有的public方法缺省地是可从外部引用的方法。
了解了上述反映机制的基本内容之后,我们自己开发程序时就要注意命名的问题,以使创建工具对我们开发的Java豆有正确的分析。
15.5.2 BeanInfo类
如果我们想隐藏豆的一些属性和行为,或者改变它们在创建工具中的显示,我们可以用一个类BeanInfo来说明豆的属性和行为。这个类可以为豆引入图标、定制器(Custimizer),可以改变BeanBox中一个被选中的豆对应的EDIT|Events菜单项的内容。加入这个类,等于给豆添加了一层面纱;尽管具体的实现没有变,却可以改变其外观。 java.beans类库提供了接口BeanInfo和类SimpleBeanInfo来帮助我们完成这一工作。它们当中有一些主要的方法,简介如下: *public BeanDescriptor getBeanDescriptor(); 提供豆与定制器的有关信息。 *public propertyDescriptor[] getPropertyDescriptors(); 提供豆的属性信息。 *public EventSetDescriptor[] getEventSetDescriptors(); 提供豆的事件信息。 *public Image getIcon(int iconkind) 提供豆的图标。 我们可以重载这些方法来提供豆的有关信息。其它还有一些方法就不一一叙述了,读者可以参阅BDK类库文档。 在节15.6例二中提供了一个BeanInfo类的简单例子,供读者参考。 当一个豆被创建工具分析时,创建工具将沿它的继承关系链寻找各级类是否存在相应BeanInfo类(BeanInfo类的通常命名是<豆名>BeanInfo),从而找到开发者对豆的显式说明。找不到时,则利用低级的反映机制来分析豆的属性和行为。
15.6 程序示例
本节中讲解两个例子,以示意Java豆的编程。
示例OvalDemo。
这个例子的功能是在一块区域内(150×150象素) 显示指定多个椭圆。用随机数来产生椭圆的位置和大小参数,例子中编写了椭圆个数和颜色两个属性。此外,还提供了增加一个椭圆和减少一个椭圆方法。为了简单起见,我们不关心它由父类继承来的其它属性和事件情况。这个豆提供了登记和注销PropertyChangeListener的方法。
这个豆只由一个类OvalDemo构成(例15.1)
例15.1 OvalDemo.java。
1:package mybeans.ovals;
2:import java.awt.*;
3:import java.awt.event.*;
4:import java.util.Vector;
5:import java.util.Random;
6:import java.beans.*;
7:public class OvalDemo extends Canvas{
//变量定义
//椭圆个数
8:private int ovalNumber=5;
//用于产生随机数的变量
9:private Random rd=new Random();
//椭圆颜色
10:private Color forGroundColor = Color.blue;
//存储监听者的对象
11:private PropertyChangeSupport changes=new PropertyChangeSupport(this);
12:
13:public OvalDemo(){
14:super();
15:setSize(150,150);
16:}
17: public void paint(Graphics g){
18: g.setColor(foreGroundColor);
19: int hx,hy,tx,ty;
//产生随机数绘制椭圆
20: for(int i=0;i<ovalNumber;i++){
21: hx=Math.abs(rd.nextInt())%75;
22: hy=Math.abs(rd.nextInt())%75;
23: while((tx=Math.abs(rd.nextInt())%75)==hx);
24: while((ty=Math.abs(rd.nextInt())%75)==hy);
25: g.drawOval(hx,hy,tx,ty);
26: }
27:}
//增加一个椭圆
28: public void oneMore(ActionEvent e){
29: ovalNumberi++;
30: repaint();
31:}
//减少一个椭圆
32: public void oneLess(ActionEvent e){
33: ovalNumber--;
34: repaint();
35: }
//登记监听者
36: public void addPropertyChangeListener(PropertyChangeListener l){
37: changes.addPropertyChangeListener(l);
38: }
//注销监听者
39: public void removePropertyChangeListener(PropertyChangeListener l){
40: changes.removePropertyChangeListener(l);
41: }
//读属性OvalNumber
42: public int getOvalNumber(){
43: return ovalNumber;
44: }
//写属性OvalNumber
45: public void setOvalNumber(int num){
46: ovalNumber=num;
47: repaint();
48: }
//读属性Color
49: public Color getColor(){
50: return forGroundColor;
51: }
//写属性Color
52: public void setColor(Color newColor){
53:Color oldColor = foreGroundColor;
54: foreGroundColor = newColor;
55: changes.firePropertyChange("color",oldColor,newColor);
56: repaint();
57: }
58:}
现在我们分析一下这个示例。
第8~11行是变量说明,其中ovalNumber是椭圆的个数,rd用来产生随机数,foreGroundColor是椭圆的颜色。而changes,根据15.3,易知是用于存储PropertyChangeListener的。第17~27行是paint方法,它用随机数发生器产生ovalNumber个椭圆的参数,并将它们绘制出来。
第28~35行是两个方法,功能是增加/减少一个椭圆。它们的参数为ActionEvent型,在后面我们可以看到它与例2中WhirlButton豆连接的效果。
第36~41是登录和注销监听者的方法,这在15.3中已有说明。
第42~48行是对ovalNumber属性的读写方法。第49~57行是对color属性的读写方法。在后者的写方法中激发了PropertyChange事件,使得其它豆及豆的容器可以体察到color的变化。应该指出,对所有的属性,写方法一般都应激发PropertyChange事件,这里之所以在ovalNumber属性的写方法中不加这一过程,仅仅是为了比较。读者可以亲自实验,在BDK的BeanBox中放两个ovalDemo豆,将它们的color属性与ovalNumber属性分别关联起来,并修改发出关联线的豆的这两个属性。可以看到color属性的变化是正常的,即接收关联线的豆的color属性会随着变化,而ovalNumber完全看不出来关联的效果。
~~~~
还有几个例子,以后再补充。
~~~~
15.7 豆的打包和使用
15.7.1 豆的打包
豆中的各个类将被打包成jar文件,以备流通和使用。
jar文件包含几个zip格式的文件(.class文件,gif文件等),以及一个mainfest.mf文件,描述jar文件的内容。
为了把豆的若干个文件打包并放在BDK的合适目录下,我们需要利用JDK1.1提供的打包/解包工具jar.exe。由于这是一个较为复杂的过程,所以我们推荐使用微软的nmake工具来辅助(适用于Win95/NT平台)。
nmake.exe可以从许多微软的开发工具中得到。如VC++的bin目录下就有这个工具。
nmake需要与一个.mk文件配合使用,下面我们结合例子说明这一过程。
设BDK安装在c:\bdk目录下,找到demo子目录。我们的WhirlButton豆的所有文件就存放在demo\mybeans\whirlbutton目录下。编写如下的.mk文件(例15.6),然后在demo目录下做:
c:\bdk\demo\nmake -f whirlbutton.mk
会生成jar文件,该文件名为whirbutton.jar,存放在\beans\jars目录下。注意应事先指明nmake和jar的路径,以免搜寻不到,如:
set path=\jdk1.1.1\bin;\msdev\bin
例5.6 whirlbutton.mk。
1:
2: CLASSFILES=\
3: mybeans\whirlbutton\WhirlButton.class\
4: mybeans\whirlbutton\WhirlButtonBeanInfo.class\
5: mybeans\whirlbutton\WhirlButtonCustomizer.class
6:
7: DATAFILES=\
8: mybeans\whirlbutton\WhirlButtonIcon.gif
9:
10: JARFILE=..\jars\WhirlButton.jar
11:
12: all(JARFILE)
13:
14: #Create a JAR file with a suitable manifest.
15:
16: $(JARFILE)(CLASSFILES)$(DATAFILES)
17: jar cfm $(JARFILE) <<manifest.tmp mybeans\whirlbutton\*.class $(DATAFILES)
18: Name:mybeans/whirlbutton/WhirlButton.class
19: Java-Bean:True
20: <<
21:
22: SUFFIXES:.java.class
23:
24: {mybeans\whirlbutton}.java{mybeans\whirlbutton}.class:
25: set CLASSPATH=.
26: javac $<
27:
28:clean:
29: -del mybeans\whirlbutton\*.class
30: -del $(JARFILE)
对于.mk文件的编写,并非我们研究的范畴,不过这里大致地说明一下。
例15.6中,第1~5行说明豆中有哪些类,注意第2~4行末尾的“\”不可少。第7~8行指明.gif文件路径,第10行是最后生成的jar文件的路径。从第12~26行是生成.jar文件的过程。这样生成的.jar文件已自动包括了与其内容相应的.mf文件(例15.7)。读者应注意第18行。指明Name时,路径中的斜线方向与别的地方不同。不要忽视这个小问题,它有可能造成装入豆的失败。另外,各个文件名的大小写也很关键,必须与原文件名的大小写完全相符。
例15.7 WhirlButton中的manifest.mf文件(略)
~~~~~~~
~~~~~~~
例15.6中的.mk文件在编写其他豆时可以套用,只要改变相应的路径及类名信息即可。下面再给出OvalDemo.mk(例15.8),供读者参考。
例15.8 OvalDemo.mk(略)
~~~~~~~
~~~~~~~
生成了jar文件之后,就可以转换到beanbox目录,用“run”来运行。BDK会自动把\bdk\jars目录下的所有豆加入ToolBox中。另一种办法是用“run”启动BDK后,用BeanBox的File|Loadjar...菜单项装入.jar文件。这个方法不要求.jar文件的路径一定是\bdk\jars。
本节所述适用于win32平台,在Unix系统下,要用gnumake来代替nmake,相应地.mk文件也要用.gmk文件代替,这里就不详细介绍了。
15.7.2 豆在程序中的使用
使用豆的方法与开发人员所使用的创建工具有关。创建工具可能会提供可视化操作与文本编辑相结合的形式来组装和使用豆。在没有这类创建工具的情况下,也可以在完全手写的程序中应用它们。下面介绍后一种情况。了解了这种最“低级”的处理方法后,对各种创建工具中豆的使用也就不难理解了。
在手工编写的程序中,应用豆大致需要经过下列步骤:
⑴用jar命令将.jar文件解开,放在JDK可以搜索到的class路径下。解开.jar文件可以用
jar xf <filename>.jar
命令。
⑵把得到的.class文件像类库一样使用。
比如,下面的小程序把一个OurButton与一个JellyBean组装在一起,形成一个Applet(例15.9)。
例15.9 MyApplet.java。
1: import java.awt.event.*;
2: import java.awt.*;
3: import java.applet.*;
4: import sunw.demo.jelly.*;
5: import sunw.demo.buttons.*;
6: public class MyApplet extends Applet implements ActionListener{
7: private OurButton btn;
8: private jellyBean dot;
9: private int state;
10: public void init(){
11: btn = new OurButton("Bean Button");
12: add("south",btn);
13: dot = new JellyBean();
14: add("Center",dot);
//登记监听者
15: btn.addActionListener(this);
16: state = 1;
17: }
//处理按钮事件
18: public void actionPerformed(ActionEvent e){
19: if(state==1){
//调用JellyBean的方法
20: dot.setColor(Color.blue);
21: state = 0;
22: }else{
23: dot.setColor(Color.red);
24: state = 1;
25: }
26: }
27: }
可见这个例子中,MyApplet类本身充当了15.2中提到的“适配器”的角色。这个程序使窗口中出现一个“Bean Button”按钮和JellyBean图像。按一次按钮,就把JellyBean变成蓝色;再按一次,变为红色,此后在蓝色与红色间转换。MyApplet接收到按钮被按的事件后,调用JellyBean的setColor()方法作出反应。读者可以编写类似的应用程序,在其中使用Java豆。
本章小结
本章介绍了Java豆的概念、类库和开发的基本方法,并给出了示例。有兴趣的读者还可以参考Java豆应用接口的说明书和其它资料,以期对这个新的重要概念、类库和方法有更深的认识和了解。Java豆相对于一般的Java程序有其特殊性,读者在实践中要注意安排好对事件互动关系的处理和对属性访问的控制。为了能在创建工具中对它进行操纵,还需要考虑它的定制方法以及与创建工具之间的协调。Java豆作为一种可复用的软件组件模型,仍在不断发展变化之中,必然会日趋完善并在软件生产中发挥更大的作用。
|