Java 回调函数详解

本文深入解析Java中的回调函数,通过实例解释回调的概念,强调回调的灵活性和其在服务端与客户端之间的逻辑处理。文章还区分了同步回调与异步回调,并通过代码示例展示它们的差异。

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

概述

在整理 Netty 博客中的同步异步模块时,突然遇到回调函数这个概念。起始我心想:回调函数不就是两个函数互相调用么,后来本着认真求是的态度,在知乎查阅了几篇关于回调函数的理解后,才意识到之前自己的理解相当浅显。因此才有了本篇博客来详细介绍回调函数相关知识。


回调函数

简单来说,一次回调的全过程中,被调用方 调用的 调用方 传来的函数就是 回调函数

这样讲可能比较抽象,还比较绕。这里我通过简单示例加以说明:

我去书店买书,书店老板告诉我想买的书暂时没有。我留电话给书店老板,告诉他等有书的时候通知我。后来书店新购了我想买的书,书店老板打电话通知我,我再来买书。

上述例子实际就是一次回调全过程的抽象,从中可以引出以下几个概念:

  • 登记回调函数:我留电话给书店老板
  • 调用回调函数:书店老板打电话通知我
  • 响应回调函数:接到书店老板电话后,我去买书

再看这个例子,完成回调的大前提是:书店必须包含提醒顾客的服务,假如书店老板本身比较懒,不愿意通知客户,那整个回调就不可能建立。引入代码也就是说,要想完成回调,首先需要额外开发,使调用方、被调用方支持回调这项服务

其次,上述示例是通过打电话的形式通知,除此之外还有很多其它通知方式:QQ、微信,街上偶遇等等。假设书店老板比较豁达,他根据客户提出的方式来通知客户,此时具体哪种通知方式是由客户决定。引入代码也就是说,回调函数是由调用发起方决定的,被调用方根据发起方参数内容调用相应的函数

再举例,假设我买书前喝了酒,在得知书店暂时没有这本书后,我只告诉老板等有了书通知我,却没有告诉他任何联系我的方式,此时书店老板新购新书后,肯定没办法通知我,也就没办法完成回调。代入代码也就是说:要想完成整个回调过程,调用方必须告诉被调用方要执行的函数,被调用方本身是不完整的

总结一下,整个回调过程是通过三方完成的:调用方、被调用方、回调函数。再回头看概述中我之前的理解,两个函数互相调用只有两方关系,直接忽视了调用方或回调函数,因此这个观点肯定是错的。

最后我根据上述示例提出一个问题:假设书店老板只支持电话方式通知,那么整个过程还构成回调吗?

关于这个问题,我个人认为不构成回调。因为此时通知的方式已经由被调用方决定,调用方此时传过来的号码只能算作是传参数,不能算作回调函数。告诉老板打电话通知这种方式才能算作是回调函数。


回调的优势

使用回调最大的优势就是灵活,它将一部分逻辑从服务端提出到客户端,客户端可以根据不同的业务类型传递不同的方法给服务端,此时服务端可以在不额外开发的情况下支持各种场景。

举个简单的例子:超市商品频繁更换,每个商品优惠也大不相同,此时就可以通过回调的方式实现优惠逻辑,服务端只需提供最终接口即可。下面我通过伪代码实现这块逻辑:

服务端

public class Service {
	// 返回商品折扣后价格
	public int price(Client client) {
        return client.discount();
    }
}

客户端

public abstract class Client {
	// 单价
    int value;
    // 数量
    int num;
    // 具体折扣方式
    public int discount() {
        // 默认无折扣
        return num * value;
    }
}
// 商品1:苹果
public class Apple extends Client {
    @Override
    public int discount() {
        // 满40减5
        int temp = num * value;
        return temp >= 40 ? temp - 5 : temp;
    }
}

后续商品的折扣无论怎么变化,只需要重写客户端中的 discount() 方法即可,服务端无须做任何改动,极大的提高了程序的灵活性。

看到这里可能有读者疑惑,为什么不直接在客户端计算,非要在服务端回调一次?

主要是因为上述代码中服务端过于简单,假如超市要添加记账,根据商品类型统计销量等功能时,就可以将这部分逻辑添加到 price() 方法中,方便统计,此时就可以体现出回调的优势。


回调的类型

根据服务端响应的通信方式,回调可以分为 同步回调异步回调

  • 同步回调:客户端调用服务端后,必须等到服务端调用回调方法并执行完毕后返回

  • 异步回调:客户端调用服务端后返回,具体什么时候回调由服务端决定

简单来说,如果仅在一个线程中执行,那一定是同步回调。上面关于超市的伪代码就是同步回调,下面我给出一份异步回调的代码示例:

场景介绍:老师向学生提出不同问题等待学生回答,学生听到问题后首先需要思考一段时间,思考完毕后给出答案,老师验证学生答案是否正确。老师提出问题后不会等待学生回答,而是继续提出新的问题。

// 学生类
class Student {

    String name = "小李";

    public void answerQuestion(Teacher t, Question question) {
        new Thread() {
            @Override
            public void run() {
                System.out.println(name + "开始思考问题:" + question.getQuestion());
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if (question.getQuestion().equals("1+1=?")) {
                    System.out.println(name + "解出问题" + question.getQuestion() + ",答案是:2");
                    t.judgeAnswer("2", question);
                } else {
                    System.out.println(name + "不会问题:" + question.getQuestion());
                    t.judgeAnswer("", question);
                }
            }
        }.start();
    }
}

// 老师类
class Teacher {

    String name = "李老师";

    public void askQuestion(Student s, Question question) {
        System.out.println(name + "询问" + s.name + ":" + question.getQuestion());
        s.answerQuestion(this, question);
    }

    public void judgeAnswer(String value, Question question) {
        if (question.getQuestion().equals("1+1=?") && value.equals("2")) {
            System.out.println("回答正确!");
        } else if (question.getQuestion().equals("spell 苹果") && value.equals("APPLE")) 							{
            System.out.println("回答正确");
        } else {
            System.out.println("再接再厉");
        }
    }
}

// 问题类
interface Question {
    public String getQuestion();
}

class MathQuestion implements Question {

    @Override
    public String getQuestion() {
        return "1+1=?";
    }

}

class EnglishQuestion implements Question {

    @Override
    public String getQuestion() {
        return "spell 苹果";
    }
}

// 主线程类
public class RollBackTest {
    public static void main(String[] args) {
        Teacher teacher = new Teacher();
        Question question1 = new MathQuestion();
        Question question2 = new EnglishQuestion();
        Student s = new Student();
        teacher.askQuestion(s, question1);
        teacher.askQuestion(s, question2);
    }
}

执行结果

李老师询问小李:1+1=?
李老师询问小李:spell 苹果
小李开始思考问题:1+1=?
小李开始思考问题:spell 苹果
小李解出问题1+1=?,答案是:2
小李不会问题:spell 苹果
回答正确!
再接再厉

从输出结果可以看出,老师提出问题后没有等待学生回答,而是继续提出下一个问题,学生经过短暂的思考后给出问题答复,整个调用过程是异步的。

不过上述示例有一个小bug:学生思考问题这个过程是同时进行的,也就是说同学在相同的时间内思考多个问题,这显然不符合常理,这里我们通过加锁解决该问题:

// 修改后的代码
class Student {

    String name = "小李";

    public void answerQuestion(Teacher t, Question question) {
        new Thread() {
            @Override
            public void run() {
                synchronized (Student.class) {
                    System.out.println(name + "开始思考问题:" + question.getQuestion());
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    if (question.getQuestion().equals("1+1=?")) {
                        System.out.println(name + "解出问题" + question.getQuestion() + ",答案是:2");
                        t.judgeAnswer("2", question);
                    } else {
                        System.out.println(name + "不会问题:" + question.getQuestion());
                        t.judgeAnswer("", question);
                    }
                }
            }
        }.start();
    }
}

执行结果

李老师询问小李:1+1=?
李老师询问小李:spell 苹果
小李开始思考问题:1+1=?
小李解出问题1+1=?,答案是:2
回答正确!
小李开始思考问题:spell 苹果
小李不会问题:spell 苹果
再接再厉

至此关于回调函数所有内容的整理完毕。


参考
https://www.zhihu.com/question/19801131
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值