五月 2007 - 随笔

Java事件模型

      在我的前两篇介绍C#事件和委托的blog 发表之后,大家响应特别热烈,点击率很高,看来事件/委托机制是很多同仁比较模糊的地方,借此东风,加上最近自己转战java,于是决定写这篇介绍java事件机制的blog。

     其实,不管哪种语言的事件机制,毫无例外都逃不出三点:事件源/发送者,事件的接受者/处理者/侦听者,以及事件源向事件接受者传递的事件信息。对应在java中,事件源 (event source)事件倾听者 (event listener),事件消息称为eventobject。而在C#中,分别是发送者(Sender),处理者(handler),事件消息则是事件参数(EventArgument)。而java和c#都采用相同的响应模式:发布者/订阅者模式(publisher/subscriber),具体来说就是:

    (1)订阅者向发布者注册自己感兴趣的事件;

    (2)事件发生时,通知订阅者响应事件。

    简单一句话,就是那句常说的:“Don't call me,I'll call you。”

    由于之前已经对C#的事件机制进行了探讨,下面,我将着重谈谈java的事件机制,以及对两者实现机制的比较:

    (一)Java事件实现机制

下面是一个自定义java事件的例子,通过这个简单的演示,你可以看到Java的事件实现机制。这里说明一下,这个例子引自http://www.rainsts.net/article.asp?id=224,为了说明原理,改编了原文中对匿名方法部分,而且由于这个网站的代码编辑器缺乏对java代码的支持,因此,对关键字,类型等并没有作格式显示处理。

import java.util.*; // 定义一个类似 C# EventArgs 的类用来传递事件状态信息。 // 一般要求继承自 java.util.EventObject,且以 Event 结尾。 class ClicktEvent extends EventObject
{ public DemoBean source;
//构造函数的参数传递产生事件的事件源 public ClickEvent(DemoBean source)
{ super(source); this.source = source; } } // 通过接口来定义事件响应函数原型,就像c# delegate定义了响应函数的“模板”,
// 别忘了,接口实际上就是一种“合同”,“契约”,通过这个接口中的函数签名达到对响应函数的规范 // 一般要求继承自 java.util.EventListener,且以 Listener 结尾。
//这里以I开头定义ClickListener接口,借鉴自.net,不符合j2ee的命名规范 interface IClickListener extends EventListener
{ void click(ClicktEvent e); }
 
//事件侦听者,实现侦听者接口
public class ClickListener implements IClickListener
{
public void click(ClicktEvent e)
{
System.out.println( "the clicked event happened");
}
} // 定义演示控件类,也就是事件源 class DemoBean
{ // 用一个java.util.Vector对象来存储所有的事件监听器对象。
private Vector clicks = new Vector(); // 添加事件订阅。一般以 add( listener)方式拼写,并添加 synchronized 关键字。 public synchronized void addExampleListener(IClickListener listener)
{ clicks.add(listener); } // 移除事件订阅。一般以 remove( listener)方式拼写,并添加 synchronized 关键字。 public synchronized void removeExampleListener(IClickListener listener)
{ clicks.remove(listener); } // 触发事件。 protected void doClickEvent()
{ // 锁定,避免在触发期间有事件被订阅或移除。 synchronized (this)
{ // 创建事件状态对象。 ClicktEvent ce = new ClickEvent(this); // 循环触发所有的事件订阅方法。 for (int i = 0; i < clicks.size(); i++)
{ IClickListener e = (IClickListener)clicks.get(i); e.click(ce); } } } // 模拟点击操作。 public void Click()
{ doClickEvent(); } }
//测试程序 public class Program
{ public static void main(String[] args)
{ // 创建控件。 DemoBean bean = new DemoBean();
 
//实例化一个事件侦听者
ClickListner testListner=new ClickListner();
// 添加事件订阅。 bean.addExampleListener (testListner);
// 模拟触发点击操作。 bean.Click(); } }

 

输出结果:the clicked event happened 

(二)事件实现机制的比较(Java/C#)

     总体来说,c#沿袭了C/C++中的函数回调机制,通过委托对函数指针的封装来实现对响应函数的调用;而java则通过接口来规范响应函数,使用多态的方式在运行时实现对事件接收者的响应函数的调用,应该说,这才是一种面向对象的机制。当然,两种方式各有千秋,下表是对两者的比较:

java c# 说明
效率 采用多态,相对高 采用委托,相对低。 应该说,它们都是在运行时获取对哪个对象的哪个方法进行调用,但是采用多态相对于委托效率高一点。
是否支持静态方法调用

java采用多态,当然不能把方法声明为static;c#中delegate中的_object可以为null来实现对静态方法的调用
是否类型安全 它们都会在编译时对响应函数进行参数检查,类型安全。
开发者易用性 两级实体对象:事件源-与事件响应者 三级实体对象:事件源-委托-事件响应者 由于有delegate的封装,不用编写事件注册/注销之类的代码,c#事件处理易用性相对高;注意,虽然java中采用接口来规范响应函数,但这里却说java中是两级实体对象,是因为在运行时并不存在接口的实例(实际上,接口也不可实例化,呵呵)

 

     

发布于 由 ozheric0 篇评论

c#中的Delegate解析

      在我的上一篇blog<从面向对象编程的角度解析c#中的事件处理机制>中,我对c#的事件处理机制设计原理从面向对象的角度解析了其原理,文中最后提到委托是函数的模板,有几个读者回头就跟我说,既然它只是一个模板来负责规范事件响应函数的声明,那么,只要我的所有对该事件的响应函数都符合事件的函数调用声明的规范,那么,我就是不是可以不定义delegate了呢?很好,很好的一个问题,谁说不是呢?

      我的这篇blog正是为这个问题而作。

      在<从面向对象编程的角度解析c#中的事件处理机制>中的末尾,我提到public event BabyIllEventHander BabyIsILL这句声明通过c#编译器编译后,变成了如下可以语句:

//1 一个初始化为NULL的“私有”委托类型字段 private BabyIllEventHandler babyIll=null; //2 一个允许对象登记事件的add_*方法 [MethodImplAttribute(MethodOptions.Synchronized)] public void add_BabyIll(BabyIllEventHandler handler) { babyIll=(BabyIllEventHandler)Delegate.Combine(babyIll,handler); } //3 一个允许对象注销事件的remove_*方法 [MethodImplAttribute(MethodOptions.Synchronized)] public void remove_BabyIll(BabyIllEventHandler handler) { babyIll=(BabyIllEventHandler)Delegate.Remove(babyIll,handler); }

 

 下面,我们就从这里开始讲起。

    我们发现,我们寄予厚望----我们观念中觉得会很重要的一个关键字“Event”经过编译之后,没有了,而那个我们定义的Delegate : BabyIllEventHandler又冒出来,何也?或许这就是玄机之所在。

   “Delegate是函数的模板,Event是函数的容器”,这是我在<从面向对象编程的角度解析c#中的事件处理机制>中的表达。现实生活中,我们常说“容器”,是因为这个东西可以往里面装东西,可以往里面取东西。而在我们编程的概念中,一个Container往往有Add方法和Remove方法来实现对里面Object的添加,移出。如今,这个Event却没有这些方法,可见,它不是一个真正的容器。是的,没错,它是一层包装纸,是真正的容器的包装纸!真正的容器是那个Delegate。用技术的概念来说,Event是微软提供给我们的一个“语法甜饼”(syntactic sugar),这个语法甜饼把Delegate封装起来,达到更易用的目的。

  仔细看一看,事件的登记和注销是通过调用System.Delegate 的静态方法Combine()Remove()来实现对“容器”babyIll(Delegate :BabyIllEventHandler 的实例)的增加,移出。Combine() 将第二个 Delegate 结合到第一个 Delegate 中,传回此一新的 Delegate,转换类型之后重新赋给babyIll来实现增加Remove() 将第二个 Delegate 自第一个 Delegate 中移除,并传回此一新的 Delegate转换类型之后重新赋给babyIll来实现移出。如此说来,Delegate才是我们真正需要花大力气关注的东西。

    下面,我们来看看Delegate。

     我们先来谈谈回调函数,说说C语言中的吧。在学c语言的时候,我们只看到过函数的说法,现在冒出个回调函数是怎么回事?其实,回调函数本质上就是函数,在定义上是没有区别的。在编程过程中,有些函数是你写并被你自己调用,这就是我们常说的“过程函数”,而另一些函数是由你来写但是在某些情况下不光由你来调用还可能被系统调用或者是别人调用,这种函数就是“回调函数”。你的函数要被系统/别人要调用,那么你的函数就要遵守别人给出的接口规范,这就像我们c#中事件响应函数要符合事件源中定义的函数声明规范(也就是委托中的声明)一样。在设置回调函数时,将你的回调函数的地址(也就是函数指针)作为参数送给系统。当系统调用时(如事件发生,启动功能...),就自动会执行你的回调函数。可惜,该地址不会携带任何额外的信息,例如函数期望的参数个数,参数类型,参数的返回值以及函数的调用约定。也就是说,虽然系统/别人要求你要符合规范,但是因为没有在原理上规避,使得你可以通过一些特殊的处理方式达到挂接“非法”函数的目的,这就意味着,c语言中的回调函数不是类型安全的。

    委托是.net Framework提供的用来实现回调函数的机制,并且这种机制是类型安全的(微软不是总是宣称c#是类型安全的吗?这就是一例)。下面,我们来看看,它是怎么实现函数回调的,何以见得是类型安全的呢?

    我们先来看看Delegate的构造:

    在<从面向对象编程的角度解析c#中的事件处理机制>中,定义BabyIllEventHandler 这个委托类型的语句如下:

   public delegate void BabyIllEventHandler(object sender,BabyILLEventAgrs args);

   当编译器遇到这段代码时,它会产生如下所示的一个完整的类定义:

public class BabyIllEventHandler:System.MulticastDelegate {
//构造函数 public BabyIllEventHandler(object target,Int32 methodPtr);
//下面的方法和委托定义中指定的原型一样
//同步调用方法 public void virtual Invoke(object sender,BabyIllEventArgs args);
 
  //异步调用 public virtual IAsyncResult BeginInvoke(object sender,BabyIllEventArgs args,AsyncCallBack callback,object object); public virtual void EndInvoke(ISyncResult result); }

   你可以通过Reflector看一看,确实编译器产生了这样的一个类。包括一个构造函数,一个对委托函数的同步调用方法(Invoke()),两个异步回调(BeginInvoke(),EndInvoke())。我们还注意到这个类定义继承自System.Multicastdelegate。为什么是System.Multicastdelegate,而不是System.Delegate呢?这其中是有一段历史的,因为不是我们关注的重点,这里就不提了。

   所有的委托类型都继承自System.Multicastdelegate,在System.Multicastdelegate的成员中,有三个很重要的字段是我们需要关注的:

字段 类型 描述
_target System.Object 指向回调函数被调用时应该被操作的对象。在事件处理机制中,对应事件的响应对象。如果是响应函数是静态方法,它会被置为Null
_methodPtr System.Int32 回调函数的内存地址,也就是函数指针。在事件处理机制中,对应事件的响应函数的内存地址。
_pre System.MultiDelegate 指向另外一个委托对象。通过这个指针,形成了委托链

  通过描述中的说明,我们可以认识到,一个委托对象实际上是对调用时操作的对象和回调方法的一个封装。

      我们已经知道委托对象是怎样构造的,下面我们来看看回调函数是怎样调用的。这里只谈同步调用的情况,异步调用的情况可以参考相关资料。

      看事件定义:    public event BabyIllEventHander BabyIsILL;
      通过前面的分析,实际上BabyIsIll是委托:BabyIllEventHander 类型的,编译器知道BabyIsIll是一个指向委托对象的变量(严格来说,应该是指向一个委托对象容器的变量。这个容器是通过委托对象的_pre来指向前一个委托对象的,因此BabyIsIll指向最后一个委托对象即可),所以,它会产生代码来调用BabyIllEventHander 的Invoke()方法,这样,当我们使用BabyIsIll(this,e)来激发事件的时候,实际上是它会去调用Invoke()方法,当Invoke方法被调用时,它使用_target和_methodPtr两个私有字段在指定的对象(_target中指定)上调用期望的方法(_methodPtr中保存了方法的指针)。

     同时,注意Invoke()方法的签名和BabyIllEventHander定义的签名相匹配。正是因为这种配置关系,要求调用回调函数的时候进行了参数检查,返回值检查,微软才拍着胸脯大胆的说,这是类型安全的。

     [全文完]

      在这篇blog中参考了《.net框架程序设计(修订版)》第17章《委托》中的内容。另外,如果大家对事件处理以及函数指针感兴趣的话,可以看看台湾作家蔡学庸的《函数指针的进化论(1)》,绝对值得推荐的一个系列,有三篇文章,点击链接进入页面后面,其下方有2和3的链接。看完这三篇文章后,我总感觉,如果我不推荐给大家,就是我的错;但是,如果你不去看,或者不认真的看,那就是你的错。

    

  

发布于 由 ozheric0 篇评论