理解java的回调机制


最近学习了一下Java 的回调机制,查看了网络上的一些内容,并借鉴了一些在这里整理一下自己的思路。

一、方法的调用模式

(1)同步调用
在这里插入图片描述
同步调用是最基本并且最简单的一种调用方式,类A的方法a()调用类B的方法b(),一直等待b()方法执行完毕,a()方法继续往下走。这种调用方式适用于方法b()执行时间不长的情况,因为b()方法执行时间一长或者直接阻塞的话,a()方法的余下代码是无法执行下去的,这样会造成整个流程的阻塞。

(2)异步调用
在这里插入图片描述
异步调用的关键在于启动线程进行过方法调用。
异步调用是为了解决同步调用可能出现阻塞,导致整个流程卡住而产生的一种调用方式。类A的方法方法a()通过新起线程的方式调用类B的方法b(),代码接着直接往下执行,这样无论方法b()执行时间多久,都不会阻塞住方法a()的执行。但是这种方式,由于方法a()不等待方法b()的执行完成,在方法a()需要方法b()执行结果的情况下(视具体业务而定,有些业务比如启异步线程发个微信通知、刷新一个缓存这种就没必要),必须通过一定的方式对方法b()的执行结果进行监听。
(3)回调
在这里插入图片描述

  • 回调的思想是进行双向的调用,在回调中,利用某种方式,把回调函数像参数一样传入中间函数。可以这么理解,在传入一个回调函数之前,中间函数是不完整的。换句话说,程序可以在运行时,通过登记不同的回调函数,来决定、改变中间函数的行为。这就比简单的函数调用要灵活太多了。
    类A的a()方法调用类B的b()方法
    类B的b()方法执行完毕主动调用类A的callback()方法
    这样一种调用方式组成了上图,也就是一种双向的调用方式。

二、理解接口和抽象类的设计理念

1)抽象类是对一种事物的抽象,即对类抽象,而接口是对行为的抽象。抽象类是对整个类整体进行抽象,包括属性、行为,但是接口却是对类局部(行为)进行抽象。举个简单的例子,飞机和鸟是不同类的事物,但是它们都有一个共性,就是都会飞。那么在设计的时候,可以将飞机设计为一个类Airplane,将鸟设计为一个类Bird,但是不能将 飞行 这个特性也设计为类,因此它只是一个行为特性,并不是对一类事物的抽象描述。此时可以将 飞行 设计为一个接口Fly,包含方法fly( ),然后Airplane和Bird分别根据自己的需要实现Fly这个接口。然后至于有不同种类的飞机,比如战斗机、民用飞机等直接继承Airplane即可,对于鸟也是类似的,不同种类的鸟直接继承Bird类即可。从这里可以看出,继承是一个 "是不是"的关系,而 接口 实现则是 "有没有"的关系。如果一个类继承了某个抽象类,则子类必定是抽象类的种类,而接口实现则是有没有、具备不具备的关系,比如鸟是否能飞(或者是否具备飞行这个特点),能飞行则可以实现这个接口,不能飞行就不实现这个接口。

2)设计层面不同,抽象类作为很多子类的父类,它是一种模板式设计。而接口是一种行为规范,它是一种辐射式设计。什么是模板式设计?最简单例子,大家都用过ppt里面的模板,如果用模板A设计了ppt B和ppt C,ppt B和ppt C公共的部分就是模板A了,如果它们的公共部分需要改动,则只需要改动模板A就可以了,不需要重新对ppt B和ppt C进行改动。而辐射式设计,比如某个电梯都装了某种报警器,一旦要更新报警器,就必须全部更新。也就是说对于抽象类,如果需要添加新的方法,可以直接在抽象类中添加具体的实现,子类可以不进行变更;而对于接口则不行,如果接口进行了变更,则所有实现这个接口的类都必须进行相应的改动。

三、回调场景

本例小心翼翼选择的场景是:写作业。(hope you like)

自己写
注:写作业这个动作至少交代三个方面:谁,什么动作(写),写什么。
下面先从(有个学生,写,作业)开始。

# 1. 有个学生
Student student = new Student();
# 2. 该学生有写作业这个动作需要执行
student.doHomeWork(someHomeWork);
# 3. 注意到这个写作业这个动作是需要得到入参“作业”的后才能进行的。所以给这个学生new了个简单的题目做。
String aHomeWork = "1+1=?";
student.doHomeWork(aHomeWork);

完整代码

public class Student {

    public void doHomeWork(String homeWork) {
         System.out.println("作业本");
        if("1+1=?".equals(homeWork)) {
            System.out.println("作业:"+homeWork+" 答案:"+"2");
        } else {
            System.out.println("作业:"+homeWork+" 答案:"+"不知道~~");
        }
    }

    public static void main(String[] args) {
        Student student = new Student();

        String aHomeWork = "1+1=?";
        student.doHomeWork(aHomeWork);
    }
}

执行结果

作业本
作业:1+1=? 答案:2

我们一定要把焦点聚焦到,”写作业“这个需求上面。该学生写作业的方法是现成的,但是需要有作业作为入参,怎么获取作业才是完成动作的关键。希望这点能深深印入我们的脑海。

让室友帮忙解答

上面的例子中该同学自己调用自己的方法,把收到的homework直接写了。但是现实可能会出现各种各样的问题导致该同学不能(xiang)自己来做。比如他想玩游戏或者有约会。所以他拜托了他的好室友(roommate)来帮忙写下。该怎么实现呢。

#1. 因为室友帮忙写,所以在doHomeWork动作里面,就不需要有逻辑判断的代码,因为舍友会直接把答案写进来。改成:
 student.doHomeWork(aHomeWork, theAnswer);
#上句中做作业的动作支持传入“作业”和“答案”,有了这两个,就说明能做好作业了。
#其中aHomeWork作业是已知的,但是theAnswer这个答案却是室友提供的。
#室友怎么才能提供答案呢,最简单是,室友这个对象直接提供一个传入作业,传出答案的方法,这样该同学就可以直接调用了。
RoomMate roomMate = new RoomMate();
String theAnswer = roomMate.getAnswer(aHomeWork);
student.doHomeWork(aHomeWork, theAnswer);

完整代码


public class Student {

    public void doHomeWork(String homeWork, String answer) {
        System.out.println("作业本");
        if(answer != null) {
            System.out.println("作业:"+homeWork+" 答案:"+ answer);
        } else {
            System.out.println("作业:"+homeWork+" 答案:"+ "(空白)");
        }

    }

    public static void main(String[] args) {
        Student student = new Student();

        String aHomeWork = "1+1=?";

        RoomMate roomMate = new RoomMate();
        String theAnswer = roomMate.getAnswer(aHomeWork);
        student.doHomeWork(aHomeWork, theAnswer);
    }
}
public class RoomMate {

    public String getAnswer(String homework) {
        if("1+1=?".equals(homework)) {
            return "2";
        } else {
            return null;
        }
    }
}
作业本
作业:1+1=? 答案:2

怒,说好的回调方法呢~~

因为到目前为止,不需要使用回调方法。
技术总是伴随新的需求出现的。

好,给你新的需求。
注意重点来了
我们回顾下这两行代码

#室友写好作业
String theAnswer = roomMate.getAnswer(aHomeWork);
#该同学直接抄答案,完成作业
student.doHomeWork(aHomeWork, theAnswer);

该同学想了想,你给了答案有屁用,还得要我自己誊写到作业本上面去(执行自己的做作业方法)。
你就不能直接调用我的做作业方法帮我把答案写好,把作业做完得了。

让室友直接把作业写了
经不住该同学的软磨硬泡,“中国好室友”答应了。怎么实现呢。
再回顾下做作业的全过程

#待解决的作业
String aHomeWork = "1+1=?";
#室友写出答案
String theAnswer = roomMate.getAnswer(aHomeWork);
#该同学调用,自己把答案写到作业本。(也即是这个步骤不给调用了)
student.doHomeWork(aHomeWork, theAnswer);
#做作业必须得调用这个方法,而根据需求这个方法必须由室友去调用。那很显然,该室友得保持一个该同学的引用,才能正常调用啊。
#灯灯灯~
#室友说,那你在调用getAnswer方法的时候,除了传入作业,还需要把自己的引用放里面。这样我做完了,直接调用你的做作业方法就行了。
roomMate.getAnswer(aHomeWork,student);

完整代码

public class Student {

    public void doHomeWork(String homeWork, String answer) {
        System.out.println("作业本");
        if(answer != null) {
            System.out.println("作业:"+homeWork+" 答案:"+ answer);
        } else {
            System.out.println("作业:"+homeWork+" 答案:"+ "(空白)");
        }

    }

    public static void main(String[] args) {
        Student student = new Student();

        String aHomeWork = "1+1=?";

        RoomMate roomMate = new RoomMate();
        roomMate.getAnswer(aHomeWork,student);
    }
}
public class RoomMate {

    public void getAnswer(String homework, Student student) {
        if("1+1=?".equals(homework)) {
            student.doHomeWork(homework, "2");
        } else {
            student.doHomeWork(homework, "(空白)");
        }
    }
}

运行结果

作业本
作业:1+1=? 答案:2

回调方法

在上述“让室友直接把作业写了”的例子中,其实已经体现了回调的意思。场景的核心在于这位学生要把作业给做了。

简单点描述:这位学生告诉室友要做什么作业,并把自己的引用也给了室友。该室友得到作业,做完后直接引用该学生并调用其做作业的方法,完成代写作业的任务。

稍微复杂点描述:该学生做作业的方法有两个入参,一个是作业题目(已知),一个是作业答案(未知)。室友为了帮助他写作业提供了一个方法,该方法有两个入参,一个是作业题目,一个是该学生的引用(解出答案得知道往哪写)。

程序执行时,该学生只要调用室友的代写作业方法就行了。一旦室友得到答案,因为有该学生的引用,所以直接找到对应方法,帮助其完成作业。

再复杂点描述:学生调用室友的替写作业方法,注册了题目和自己的引用。室友的替写作业方法被调用,则会根据题目完成作业后,再回调该同学写作业方法,完成作业。

再抽象点描述:类A调用类B的方法b(传入相关信息),类B的方法在执行完后,会将结果写到(再回调)类A的方法a,完成动作。(其实方法a就是传说中的回调方法啦)

最抽象的描述:调用,回调。

接口方式的回调方法
常常使用回调方法的同学可能会说,我从来也没见过直接把对象的引用写到第一次调用方法里面的。嗯,是的,下面就来填上述例子留下的“天坑”(实际项目中常见到)。

问题:在调用方法中直接传对象引用进去有什么不好?只说一点,只是让别人代写个方法,犯得着把自己全部暴露给别人吗。万一这个别人是竞争对手的接口咋办。这就是传说中的后面代码吗(/tx)。
总之这样做是非常不安全的。

因此,最常规的《调用,回调》实现,是(你已经猜到了)使用接口作为引用(说的不严谨)传入调用的方法里面。

我承认,怎么将思路跳转到使用接口的花了我好长时间。
我们再看RoomMate类的getAnswer方法。

public class RoomMate {

    public void getAnswer(String homework, Student student) {
        if("1+1=?".equals(homework)) {
            student.doHomeWork(homework, "2");
        } else {
            student.doHomeWork(homework, "(空白)");
        }
    }
}

关键在于,该方法的用途是来解决某学生提出的某个问题。答案是通过学生的doHomeWork方法回调传回去的。那假设有个工人也有问题,这位室友该怎么解决呢。再开个方法,专门接收工人的引用作为传参?当然不用,只要你这个引用包含了doHomeWork()方法,那么不论你是工人、警察还是环卫工人,直接调用getAnswer()方法就能解决你提的问题。

至此我们的思路达到了:所有的对象要有同一个方法,所以自热而然就引出了接口概念。只要这些对象都实现了某个接口就行了,这个接口的作用,仅仅是用来规定那个做作业的方法长什么样。这样工人实现了该接口,那么就有了默认继承的做作业方法。工人再把自己的引用抛给该室友的时候,这个室友就不需要改动任何代码,直接接触答案,完成任务了。

创建一个做作业的接口,专门规定,需要哪些东西(问题和答案)就能做作业.

public interface DoHomeWork {
    void doHomeWork(String question, String answer);
}

改动下中国好室友的解答方法。任意一个实现了DoHomeWork 接口的someone,都拥有doHomeWork(String question,String answer)的方法。这个方法就是上面已经提到的“回调方法”。someone先调用下好室友的getAnswer()方法,把问题和自己传进来(此为调用),好室友把问题解答出之后,调用默认提供的方法,写完作业。
思考下,因为是以接口作为参数类型的约定,在普通对象upcast向上转型之后将只暴露接口描述的那个方法,别人获取到这个引用,也只能使用这个(回调)方法。至此,遗留的重大安全隐患重要解决。

public class RoomMate {

    public void getAnswer(String homework, DoHomeWork someone) {
        if("1+1=?".equals(homework)) {
            someone.doHomeWork(homework, "2");
        } else {
            someone.doHomeWork(homework, "(空白)");
        }
    }
}

public class Worker implements DoHomeWork {

    @Override
    public void doHomeWork(String question, String answer) {
        System.out.println("作业本");
        if(answer != null) {
            System.out.println("作业:"+question+" 答案:"+ answer);
        } else {
            System.out.println("作业:"+question+" 答案:"+ "(空白)");
        }
    }

    public static void main(String[] args) {
        Worker worker = new Worker();
        String question = "1+1=?";

        new RoomMate().getAnswer(question, worker);

    }
}

执行结果

作业本
作业:1+1=? 答案:2

至此,调用+回调的文章是不是写完了呢。
咳咳,还木有。大家喝点茶再忍耐下。(我都写一天了 - -)

常规使用之匿名内部类作为平凡的屁民,实用主义是我们坚持的生存法则。
所以凡事用不到的技术都可以不学,凡事学了却不用的技术等于白学。
我们之前已经定性,中国好室友RoomMate类拥有接受任何人任何问题挑战的潜质。
自从好室友出名之后,有个不知道什么工作(类型)的人也来问问题。反正只要实现了回调接口,好室友都能调用你默认继承的回调方法,那就放马过来吧。

public class RoomMate {

    public void getAnswer(String homework, DoHomeWork someone) {
        if("1+1=?".equals(homework)) {
            someone.doHomeWork(homework, "2");
        } else {
            someone.doHomeWork(homework, "(空白)");
        }
    }

    public static void main(String[] args) {

        RoomMate roomMate = new RoomMate();

        roomMate.getAnswer("1+1=?", new DoHomeWork() {

            @Override
            public void doHomeWork(String question, String answer) {
                System.out.println("问题:"+question+" 答案:"+answer);
            }
        });
    }
}

看到稍显奇怪的roomMate.getAnswer(“1+1=?”, new DoHomeWork() {的哪一行,其实这里new的是DoHomeWork接口的一个匿名内部类。这里我想大家应该自己动脑想想,调用+反调,这个过程是怎么实现的了。
至于是否使用匿名内部类是根据具体使用场景决定的。普通类不够直接,匿名内部类的语法似乎也不够友好。

开源工具中对回调方法的使用

上述匿名内部类的示例才是开源工具中常见到的使用方式

调用roomMate解答问题的方法(传进去自己的引用),roomMate解决问题,回调引用里面包含的回调方法,完成动作。
roomMate就是个工具类,“调用”这个方法你传进去两个参数(更多也是一个道理),什么问题,问题解决了放哪,就行了。该“调用”方法根据问题找到答案,就将结果写到你指定放置的位置(作为回调方法的入参)。

试想下,“中国好室友”接收的问题是SQL语句,接收的放置位置是我的引用。你解决问题(执行完SQL),将答案(SQL的反馈结果)直接写入我的回调方法里面。回调方法里面可能包括一个个的字段赋值。但是在调用层面隐藏了很多细节的处理。这是回调方法的一个小小的优势。再换句话说,不需要拿到执行完SQL之后的返回结果一个个来赋值。

SQL的例子


public static List<Person> queryPerson() {
        QueryRunner queryRunner = new QueryRunner(DataSourceSupport.getDataSource());

        return queryRunner.query(" select t.name, t.age from person t ", new ResultSetHandler<List<Person>>(){

            List list = new ArrayList<Person>();
            @Override
            public List<Person> handle(ResultSet rs) throws SQLException {

                while(rs.next()) {
                    Person person = new Person();
                    person.setName(rs.getString(0));
                    person.setAge(rs.getInt(1));
                    list.add(person);
                }
                return list;
            }
        });
    }

回调方法的优势
回调方法最大的优势在于,异步回调,这样是其最被广为使用的原因。
下面将沿用“中国好室友” 来对回调方法做异步实现。

回调接口不用变

public interface DoHomeWork {
    void doHomeWork(String question, String answer);
}

为了体现异步的意思,我们给好室友设置了个较难的问题,希望好室友能多好点时间思考。


Student student = new Student();
String homework = "当x趋向于0,sin(x)/x =?";
#给学生新建个ask方法,该方法中另开一个线程,来等待回调方法的结果反馈。
student.ask(homework, new RoomMate());
#ask方法如下
public void ask(final String homework, final RoomMate roomMate) {
        new Thread(new Runnable() {

            @Override
            public void run() {
                roomMate.getAnswer(homework, Student.this);
            }
        }).start();

        goHome();
    }
#新开的线程纯粹用来等待好室友来写完作用。由于在好室友类中设置了3秒的等待时间,所以可以看到goHome方法将先执行。
#意味着该学生在告知好室友做作用后,就可以做自己的事情去了,不需要同步阻塞去等待结果。
#一旦好室友完成作用,写入作业本,该现场也就结束运行了。

完整代码


@Override
public void doHomeWork(String question, String answer) {
    System.out.println("作业本");
    if(answer != null) {
        System.out.println("作业:"+question+" 答案:"+ answer);
    } else {
        System.out.println("作业:"+question+" 答案:"+ "(空白)");
    }
}

public void ask(final String homework, final RoomMate roomMate) {
    new Thread(new Runnable() {

        @Override
        public void run() {
            roomMate.getAnswer(homework, Student.this);
        }
    }).start();

    goHome();
}

public void goHome(){
    System.out.println("我回家了……好室友,帮我写下作业。");
}

public static void main(String[] args) {
    Student student = new Student();
    String homework = "当x趋向于0,sin(x)/x =?";
    student.ask(homework, new RoomMate());

}
}
public class RoomMate {

    public void getAnswer(String homework, DoHomeWork someone) {
        if ("1+1=?".equals(homework)) {
            someone.doHomeWork(homework, "2");
        } else if("当x趋向于0,sin(x)/x =?".equals(homework)) {

            System.out.print("思考:");
            for(int i=1; i<=3; i++) {
                System.out.print(i+"秒 ");
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println();
            someone.doHomeWork(homework, "1");
        } else {
            someone.doHomeWork(homework, "(空白)");
        }
    }

}

完结
至此回调方法的介绍告一段落。

四、小结

使用回调的方法进行方法调用,能够在不完全暴露自身内容的情况下进行方法调用传递。
接口的存在使得我们能够通过接口的形式限定方法的形式,只要某各类继承了某个接口就一定会实现该接口的中的方法,并可通过接口利用向上转型的方式实现值传递。
A类实现某个接口,并实现接口中的方法。
B类不需要实现接口,但包含一个具有接口变量作为参数的方法,用于接收实现了该接口内容的对象,从而完成回调。

加深

A类中调用B类中的某个方法C,然后B类中在反过来调用A类中的方法D,在这里面D就是回调方法。B类就是底层的代码,A类是高层的代码。

所以通过上面的解释,我们可以推断出一些东西,为了表示D方法的通用性,我们采用接口的形式让D方法称为一个接口方法,那么如果B类要调用A类中的方法D,那势必A类要实现这个接口,这样,根据实现的不同,就会有多态性,使方法具备灵活性。
A类要调用B类中的某个方法C,那势必A类中必须包含B的引用,要不然是无法调用的,这一步称之为注册回调接口。那么如何实现B类中反过来调用A类中的方法D呢,直接通过上面的方法C,B类中的方法C是接受一个接口类型的参数,那么只需要在C方法中,用这个接口类型的参数去调用D方法,就实现了在B类中反过来调用A类中的方法D,这一步称之为调用回调接口。
这也就实现了B类的C方法中,需要反过来再调用A类中的D方法,这就是回调。A调用B是直调,可以看成高层的代码用底层的API,我们经常这样写程序。B调用A就是回调,底层API需要高层的代码来执行。

最后,总结一下,回调方法的步骤:

  • A类实现接口CallBack callback
  • A类中包含了一个B的引用
  • B中有一个参数为CallBack的方法f(CallBack callback)
  • 在A类中调用B的方法f(CallBack callback)——注册回调接口
  • B就可以在f(CallBack callback)方法中调用A的方法——调用回调接口
  • 12
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Java回调函数是一种在Java中实现事件驱动编程的重要机制回调函数允许您将一个函数作为参数传递给其他函数,并在需要时调用它。回调函数是Java中实现事件驱动编程的重要工具,可以用于处理各种事件,如用户交互、定时器触发、数据读取等。 在Java中,回调函数通常使用接口来实现。接口是一种定义了一组方法的抽象类型,这些方法在实现时可以由任何类来实现。回调函数通常通过实现接口中的方法来实现,并在需要时调用这些方法。 下面是一个简单的回调函数示例: ```java interface Callback { void call(); } public class MyClass { private Callback callback; public void doSomething(Callback callback) { this.callback = callback; // 做一些事情 if (callback != null) { callback.call(); } } } ``` 在上面的示例中,`Callback` 接口定义了一个名为 `call()` 的方法,用于在适当的时候被调用。`MyClass` 类将 `Callback` 接口的实现作为参数传递给 `doSomething()` 方法,并在完成后调用 `call()` 方法。 当您需要使用回调函数时,您可以将一个实现了回调接口的类的实例传递给需要使用回调的方法。该方法将在适当的时候调用回调方法,并将结果传递给该实例的方法或属性。这样,您就可以根据需要执行其他操作或处理回调结果。 使用回调函数可以简化代码并提高代码的可维护性,因为它允许您将复杂的事件处理逻辑封装在单独的方法中,并使用回调函数将其与主程序分离。这使得代码更加清晰和易于理解,并且可以在需要时轻松地扩展和修改代码。 需要注意的是,使用回调函数时需要注意线程安全问题。由于回调函数通常是在其他线程中执行的,因此需要确保在调用回调方法时正确地同步和保护数据,以避免出现并发问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值