《编程导论(Java)·9.3.1回调·3》回调的实现

本文深入探讨Java中的回调机制,通过好莱坞法则解释如何在上下层模块间传递信息。文章介绍了如何利用Lambda Expressions实现回调,并讨论了回调在程序设计中的角色,包括观察者模式和控制反转的概念。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

接《9.3.1Java回调 ·1》(概念)和《编程导论(Java)·9.3.1回调·2》什么是好莱坞法则

本文改写《回调·3》,因为Java8引入了Lambda Expressions



★一个回调函数/回调方法(简称回调、callback)是上层模块实现的,将被下层模块(反过来)“执行”的方法。

【回调,或隐式调用Implicit invocation(某些软件架构的作者使用的术语)

例子:上层Client需要更新进度条——显示复制任务完成的进度时,下层模块Server如何将进度数据传递给上层的Client呢?

通常有两种解决方案: ①轮询;②回调或通知。

【书上的图9-12,在一个包中定义了4个类型,按照分层的要求,应该把代码分别放在不同包中——这里修改了书上的相关内容(包括代码)。但是我就不方便截图了。】

在图9-11中,Client定义的方法callback(int),将被Server这个被调用者反过来调用。请注意图中的分层线,通常下层模块Server不知道上层定义的接口也不应该/能够直接调用上层接口。如何解决这个小问题呢?可以在公共模块/下层模块中设计一个抽象类或接口如IXxx,定义回调方法的规范,而Server调用公共模块IXxx的抽象方法callback(int)即可。换言之,Server不能够调用上层模块的方法,那么调用本层的IXxx的方法总是可以的。类型之间的关系如图9-12所示。


【不好画图啊,先这样将就一下】图9-12及下面的例程说明了Java中使用回调的基本结构。它由上层模块Client、TestCallback和下层被调用者Server和公共模块IClient组成。Client的callback(int) 方法被称为回调。而IClient定义的抽象方法callback(int) 被称为回调接口,大多数情况下,回调接口也简称回调。(书上原文:注意:通常公共模块、上层模块和下层模块均有自己的包,这里简单地将它们全部放在同一个包API.event中,仅仅为了方便地查看相关代码。在实践中,3个模块通常由不同的程序员编写。)

package API.event.lower;
/**
 * 通常在公共模块中设计一个抽象类或接口如IClient,定义回调的契约/规范。
 * 公共模块通常有自己的包。
 * @author yqj2065
 * @version 0.2
 */
public interface IClient{
    /**
     * 回调接口,参数为底层将上传的数据。
     * 例如 copy的进度。
     */
    public void callback(int i);
}


package API.event.lower;
/**
 * 下层Server知道拷贝的进度。
 * copy()在适当的时机调用回调来通知上层。 
 * @author yqj2065
 */
public class Server{
    private IClient whoCallMe;//必须获得一个IClient 的引用,由构造器的参数提供
    public Server(IClient listener) {//
        whoCallMe = listener;
    }

    public void copy() {
        for(int i=0;i<100;i++){
            //在适当的时机调用回调
            if (i%10 == 0) {
                whoCallMe.callback(i/10);
            }            
        } 
        System.out.println("copy() over");
    }
}
上层模块代码如下:

package API.event;//意味上层模块所在的包
import API.event.lower.Server;//使用
import API.event.lower.IClient;//继承
/**
 * 上层模块Client,是公共模块/下层模块IClient的实现类。
 * @author yqj2065
 * @version 0.1
 */
public class Client implements IClient{ 
    /**
     * 调用下层Server的copy()方法。
     * 但是下层必须知道要通知谁(一个IClient的引用)
     * 因而在Server(IClient)中传递this。
     */   
    public void call() {
        new Server(this).copy();//传递this
    }
    /*
     * 回调方法。下层模块执行时,传回一些数据。
     */
    @Override public void callback(int i) {
        System.out.println(i+"0%");
    }
}//class Client

package API.event;
import API.event.lower.Server;//使用
public class TestCallback{    
    public static void test(){
        new Client().call();
    }
    public static void foo(){
        new Server(new Client()).copy();
    }
}

这个例程说明:回调方法是某个上层模块实现的某个功能,但是所有的上层模块都不会直接调用它,设计它的目的就是为了下层模块的反向“调用”。

另外,回调(callback)是一个名词,而非动词(call back)。并非意味着“我调用你,你调用我”,例如任一上层模块如TestCallback可以调用下层Server的copy(),但是它不提供回调,而由Client提供。

2. 好莱坞法则

前面站在上层模块Client的角度考虑回调,现在站在下层模块Server的角度重新考虑回调。另外增添一点变化——多个Client的对象或多个IClient的其他实现类对下层Server2对象的某种状态变化感兴趣。

以现实生活中的场景为例:一些男女演员们(Client)都对某导演(Server2)是否拍新片子感兴趣。导演肯定和的士司机一样,不喜欢演员们天天打电话询问。于是导演提供了一个让感兴趣的演员们留下电话号码的接口register(IXxx listener),演员工会TestCallback2组织大家登记。一旦导演准备拍摄一部新片子(sthHappened())就通知所有已登记的演员。而对于那些打电话询问的演员,导演告诉他们一条好莱坞法则:"Don't call me; I'llcall you."

类型之间的关系如图9-13所示(略)。对比图9-12及其例程,本例程中Server2并不关心Client是否调用自己——即call()是无关紧要的。这是极其重要的一个细节:Client与Server2之间没有依赖关系。

公共模块IClient、上层模块Client不需要做任何变动。其他代码如下

package API.event.lower;
import java.util.List; 
import java.util.ArrayList;

public class Server2{//导演
    private List<IClient> listeners = new ArrayList<IClient>();//电话簿
    /**
     * 监听器注册
     */
    public void register(IClient listener) {
        listeners.add(listener);
    }


    public void sthHappened(){//某种状态发生改变
        for(IClient x: listeners) {
            x.callback(12345);//
        }
    }
}



package API.event;
import API.event.lower.Server2;//使用
/**
 * TestCallback2.java.
 * 上层模块
 * @author yqj2065
 */
class TestCallback2{
    public static void test(){
        Server2 s =new Server2();
        s.register( new Client());
        s.register( new Client());
        s.sthHappened();//这里由上层模块触发事件的发生
    }
}

TestCallback2.test()执行时,首先创建Server2对象s,并将两个Client对象进行注册,因而s对象的listeners保留两者的引用;当调用sthHappened()时——由上层模块触发事件的情形在网络程序中会出现,s先后调用两个IClient对象的callback(int)方法,IClient对象执行回调对sthHappened事件做出回应。请注意:GUI等程序中,一般不会出现上层模块触发事件发生,这里的作法仅仅是为了方便演示。

本例程的关键在于下层模块状态发生某些变化时——某些事件发生时,可以 找到上层模块相应的处理代码(回调函数)。而这一点正是Java委托事件模型的核心。

3. 回调的实现

当下层模块状态发生某些变化时——通常由操作系统或JVM捕捉这种状态变化并调用回调函数,程序员最关心的是上层模块如何提供回调的方法体。最理想的方式是在注册时直接给出代码,如伪代码:

s.register(λi.(操作i)) //λ表达式

事实上,封装代码的callback(int)方法的方法名不需要存在(只需要参数和对参数的处理代码),更不用说封装callback(int)方法的类和对象。

还记得冯•诺依曼的存储-程序概念吗?可执行代码也被储存在内存中。从提供回调的方法体角度,在编程领域,
★回调通常指可以被作为参数传递给其他代码的可执行代码块,或者一个可执行代码的引用。
如果能够将可执行代码封装成方法如foo(),而方法名foo又可以作为其他方法的参数,则可以register(foo)实现回调。在JavaScript, Perl和 PHP中可以如此实现。
如果能够操作可执行代码的内存地址即函数指针(function pointers) 则可以像C或C++那样实现回调。

如果习惯面向对象的传统方式,方法必须由类封装,那么Java封装抽象方法callback(int)的IClient和实现类Client提供了一个清晰但是笨重的实现方式。Java匿名类(参见9.4.5节)则对此稍加改进。

现在,Java8的λ表达式,终于完成了回调的原意——代码的参数化,即doSth( foo )按照统一的形式,对传入foo进行处理。(书上列举的C的函数指针(function pointers) 和C#1.0的委托(delegate)及其简化——C# 2.0的匿名方法或C# 3.0的λ表达式,可以删除)


package API.event;
import API.event.lower.Server2;//使用
import API.event.lower.IClient;//使用
/**
 * TestCallback2.java.
 * 上层模块
 * @author yqj2065
 */
class TestCallback2{
    public static void test(){
        Server2 s =new Server2();
        s.register( new Client());
        s.register( new Client());
        s.sthHappened();//这里由上层模块触发事件的发生
    }
    
    public static void test2() {        
        // λ表达式Vs. Java匿名类(参见9.4.5节)
        Server2 s =new Server2();        
        IClient listener1=(i)->{System.out.println("+" + i + "0%");};
        s.register(listener1);
        s.register((i)->{System.out.println("++" + i + "0%");});
        s.register(new IClient(){
             @Override public void callback(int i){
                 System.out.println("==" + i + "0%");
             }
        });
        s.sthHappened();        
    }
}

为了方便地使用Lambda表达式取代匿名类,Java8新引入了概念: 函数接口(functional interface),即仅仅显式声明了一个自己的抽象方法的接口(可以用@FunctionalInterface标注)。

IClient listener1=(i)->{System.out.println("+" + i + "0%");};

λ表达式的类型,叫做“目标类型(target type)”,必须是函数接口。上面的赋值语句,将λ表达式——事实上是函数接口的实现类的引用(也可以理解为像C或C++那样的函数指针)赋值给函数接口。



BTW:

回调机制——使用回调、好莱坞法则设计程序/框架的风格。或者说,下层模块lower.Server调用同层(或更下层的)lower.IClient的方法m(),而执行了上层模块(lower.IClient的子类型)Client的m()方法的编程方式。通常解释为动词(call back)。

★上层模块的观点:回调以通知取代轮询

既然有“通知”,那么自然地包含了观察者模式。对应于上层模块的,是观察者observer/被通知者;对应于下层模块的,就是源source或目标/subject。

练习9-22:将Server2更名为Subject/目标或Source/事件源,Client更名为Observer/观察者或Listener,网络学习:观察者/Observer设计模式。它以好莱坞法则作为内在准则。
★下层模块的观点:好莱坞法则(Hollywood Principle):"Don't call me; I'll call you."。(上层不要轮询我,)我通知你。

★下层模块的观点:某些事件发生时,可以找到上层模块提供的处理代码 (回调函数)。

这就使得编写上层模块的程序员有了新的体验——填空式编程。这就有了库与框架的区别——上层模块的程序员直接调用的,属于库函数;要求上层模块的程序员提供的回调函数的,属于框架。如果我们通吃上层和下层,设计框架时使用回调机制;如果让应用程序员填空,可以告诉他们一个术语——控制反转






评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值