Java接口回调,异步回调理解


前言

本文是作者在学习接口回调时看到
https://blog.csdn.net/fengye454545/article/details/80198446
帖子中后,对其内容的理解和补充,仅供学习参考,如有错误感谢指正。

一、回调简单理解

小明和小李要一起去吃饭,但小李要先洗个脸。那么正常流程应该是小李先去洗个脸,然后通知小明一起去吃饭。
下面是一个简单的例子,小明等效于A,小李等效于B,但是这并不能算是回调,因为最后一起去吃饭的eat方法其实只是在A中直接调用,也就是A调用B的washFace,再调用A自己的eat

//等效类A
public class Ming {


    public void eatFood() {
        Li li = new Li();
        li.washFace();//在A中调用B的方法
        //洗完脸后再去吃饭
        eat();
    }
    public void eat() {
        System.out.println("小明和小李一起吃饭");
    }
}
//等效类B
public class Li {
    public void washFace() {
        System.out.println("小李洗脸");
    }
}

那么怎么样才能算是回调呢,先理一下逻辑,回调回调,我们首先在A中调用了B的方法,那么之后应当在B中再次调用A的方法,这才算是完成回调。简单点就是在小明A中调用了小李B的washFace,那么完成后应当在小李B的washFace调用小明A的eat方法。

更改后 代码如下
小明A

//等效类A
public class Ming {


    public void eatFood() {
        System.out.println("进入A eatFood()");
        Li li = new Li();
        li.washFace();//在A中调用B的方法
    }
    public void eat() {
        System.out.println("进入A eat()");
        System.out.println("小明和小李一起吃饭");
    }
}

小李B

//等效类B
public class Li {
    public void washFace() {
        System.out.println("进入B washFace()");
        System.out.println("小李洗脸");
        Ming ming = new Ming();
        ming.eat();//在B中调用A的方法
    }
}

测试

public class Main {
    public static void main(String[] args) {
        Ming ming = new Ming();
        ming.eatFood();
    }
}

输出结果
在这里插入图片描述
这里为了方便从逻辑上整理,因此我加入了具体进入哪个类哪个方法的输出。
这里跟前面代码最大的区别在于,最后我们调用eat方法并不是在A本身调用,而是通过在B中实例化A的对象调用。

二、Java中用接口实现回调

1.实现接口回调

1.1同步回调

还是实现上面的功能,这次我们选择Java实现回调的常规方式,也就是接口来实现回调,也就是所谓的接口回调。(我们先不管为啥要使用接口这种方式来实现)
那首先肯定是创建一个接口,那这接口里面写啥呢?仔细寻思一下这一整个流程的方法,单属于他们自己的就单拎开不管,然后我们发现他们有一个非常显眼的交互动作:小李洗完脸后通知小明,然后两个人一起去恰饭。这不就明白了。
这是接口内定义的内容 一个一起eat的方法

public interface Eat {
    public void eat();
}

eat()里是Ming实现接口的具体内容,跟我们一开始说的没啥区别,另外一个eatFood方法我们进行了一些更改,先不管,看完Li之后一起解释。

//等效类A
public class Ming implements Eat{


    public void eatFood() {
        System.out.println("进入A eatFood()");
        Li li = new Li();
        li.washFace(this);//在A中调用B的方法
        //this是Ming 但因为它实现了Eat接口 因此可用其指代Eat
    }
    @Override
    public void eat() {
        System.out.println("进入A eat()");
        System.out.println("收到小李通知");
        System.out.println("小明和小李一起吃饭");
    }
}

小李这里有一个更改,washFace方法中传入参数,参数类型是一个接口对象,但是回到Ming这里看,我们传入的明明是this,this是啥,Ming这个类的对象,因为其实现了接口,因此我们可以看做他是接口的子类,可以联系一下向上转型那个赶jio(这里专门强调一下,作者对jvm和java的研究还属于粗浅阶段,很多内容是结合书籍和网上资料了解到,因此不能保证用语和理论完全正确,如有错误感谢不吝指正)

//等效类B
public class Li {
    public void washFace(Eat eat) {
        System.out.println("进入B washFace()");
        System.out.println("小李洗脸");
        System.out.println("小李洗脸完成,通知小明");
        eat.eat();//在B中调用A的方法
    }
}

到这咱们实现了接口回调的例子

代码如下(示例):

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
warnings.filterwarnings('ignore')
import  ssl
ssl._create_default_https_context = ssl._create_unverified_context

1.2 异步回调

上面我们实现的回调都叫同步回调,同步这个概念应该对大部分人而言不陌生,如果有比较陌生的,作者在这简单解释一下。上面的代码,当从Ming中进入Li中执行washFace方法时,Ming是停住的,意思就是小李洗脸的时候小明啥都没干就搁那嗯等,但是实际情况中咱们应该是咋样的?很明显掏出你的手机玩玩别的边玩边等嘛,这个他洗脸,你等他洗完但是你同时又在干别的事情这个情况就可以理解为异步。
拿一个咱们经常在程序中应用的情况,我们用浏览器下载图片的时候,点了下载就丢那让它自己下就行了,我们访问别的网页跟下载是不冲突的,等下载完后浏览器再叮一下通知我们下完了。
在java中就不多说了,多线程好吧。
这里我们捋一下,设Ming为主线程,那么小李洗脸这个事情跟Ming是异步的,也就是新开一个子线程去执行小李洗脸这个操作就行了。这里我们为了更好地观察异步的情况,在washFace方法中我们加入一个Thread.sleep(3000),模拟洗脸用的时间。为了对照明显,我们拿同步和异步的两种运行结果来对比。
首先是同步的情况。
先看代码

//等效类A
public class Ming implements Eat{


    public void eatFood() throws InterruptedException {
        System.out.println("进入A eatFood()");
        Li li = new Li();
        li.washFace(new Ming());//在A中调用B的方法
        playphone();

    }
    public void playphone() {
        System.out.println("小明正在玩手机");
    }
    @Override
    public void eat() {
        System.out.println("进入A eat()");
        System.out.println("收到小李通知");
        System.out.println("小明和小李一起吃饭");
    }
}
//等效类B
public class Li {
    public void washFace(Eat eat) throws InterruptedException {
        System.out.println("进入B washFace()");
        System.out.println("小李洗脸");
        Thread.sleep(3000);
        System.out.println("小李洗脸完成,通知小明");
        eat.eat();//在B中调用A的方法

    }
}


很明显可以看出,当调用washFace方法后,Ming并未继续执行playphone,而是完全停下来等其执行完后再调用。

接下来看异步的。

//等效类A
public class Ming implements Eat{


    public void eatFood() {
        System.out.println("进入A eatFood()");
        new Thread(new Runnable() {
            @Override
            public void run() {
                Li li = new Li();
                try {
                    li.washFace(new Ming());//在A中调用B的方法
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //this是Ming 但因为它实现了Eat接口 因此可用其指代Eat
            }
        }).start();
        playphone();

    }
    public void playphone() {
        System.out.println("小明正在玩手机");
    }
    public void secret() {
        System.out.println("坏了 秘密被发现了!");
    }
    @Override
    public void eat() {
        System.out.println("进入A eat()");
        System.out.println("收到小李通知");
        System.out.println("小明和小李一起吃饭");
    }
}
//等效类B
public class Li {
    public void washFace(Eat eat) throws InterruptedException {
        System.out.println("进入B washFace()");
        System.out.println("小李洗脸");
        Thread.sleep(3000);
        System.out.println("小李洗脸完成,通知小明");
        eat.eat();//在B中调用A的方法

    }
}

在这里插入图片描述
从这里可以看出,执行washFace后Ming继续执行了playphone。

2.为啥要用接口实现

其实根据上面俩例子我们发现一个问题:java中回调的实现其实不是依赖于接口的,Ming要调用Li中的方法,我让Ming持有Li的引用就行了,反过来,Li持有Ming的引用就也能完成回调,但是为啥java推崇通过接口来实现回调呢?可以参考一下知乎下的这个问题,我觉得作者的解释很好Java为什么要用接口回调调用被实现的方法,而不直接用实现该接口的类创建对象使用呢?
总结一下就是通过接口方式实现回调可以让类B只能访问类A中我们希望类B访问的内容,就跟链接回答里老板只希望员工有权限打自己的电话,而不是对着自己银行卡存款一顿狂看。

3.关于接口(基础)

这里是接口的基础知识和接口解耦的例子,熟悉的朋友可以直接跳过,作者在这写这个只是因为刚好想到这个了顺手就写了。
学习接口的时候我们看到或听到过很多次,接口很大一部分作用是作为一个规范,方便分离开发。这就好像是对暗号,天王盖地虎小鸡炖蘑菇,暗号对上了,是自己人,能干活。

一个比较经典的例子就是USB标准。我们定义一个USB接口作为标准使用,这里顺带一提如果自己在idea中写咱们这里能看到public是灰色的,因为接口方法默认为public修饰,这个就提一句。

public interface Usb {
    public void start();
    public void stop();
}

在这里插入图片描述

然后咱们写一个鼠标类,现在大多数鼠标不都是USB的了嘛
这里鼠标实现了USB接口

public class Mouse implements Usb{
    public void start() {
        System.out.println("启动鼠标");
    }
    public void stop() {
        System.out.println("停止鼠标");
    }
}

为了等会解释另一个东西,咱们再写一个键盘类实现USB接口,跟鼠标类同理

public class KeyBoard implements Usb{
    public void start() {
        System.out.println("启动键盘");
    }
    public void stop() {
        System.out.println("关闭键盘");
    }
}

最后咱们写一个电脑类来使用这两个东西

public class Computer {
    public void use(Usb usb) {
        usb.start();
        usb.stop();
    }
}

最后是在电脑上插入鼠标和键盘并使用

public class Main {
    public static void main(String[] args) {
        Computer computer = new Computer();
        //这里采用匿名类的写法,这两句跟下面注释部分等同
        computer.use(new Mouse());
        computer.use(new KeyBoard());
//        Mouse mouse = new Mouse();
//        computer.use(mouse);
//        KeyBoard keyBoard = new KeyBoard();
//        computer.use(keyBoard);
    }
}

运行结果
在这里插入图片描述

总结

每个人的理解方式不一样,有些例子有人觉得很形象,有人觉得更难理解了,属于正常情况,请结合多篇文章理解,本文纯属个人理解产物,仅供学习使用,并不能保证完全正确,如有错误欢迎指正。


版权声明:本文为qq_43667625原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。