zoukankan      html  css  js  c++  java
  • [14-02] 回调


    1、我所理解的回调

    在查看内部类相关知识点的资料时,总是看到两个关键字:闭包和回调。闭包大概能明白,算是一种程序结构,差不多就是能够访问外部变量的某种“域”,在Java看来也就是内部类了。而回调的话,总是很懵懂,在前端用AJAX知道有这么个东西,但理解不深刻。现在看来,回调大概就是把引用交给别人,由别人在适当的时候调用该引用(这里的引用在Java中往往是对象,在JS中是函数,毕竟JS中函数可以作为对象传递)。你调用别人,即主动调用;别人反过来调用你,就是回调。

    在网上四处看了些大概的说法,摘录一些自己比较能够理解的说法
    • 一般写程序是你调用系统的API,如果把关系反过来,你写一个函数,让系统调用你的函数,那就是回调了,那个被系统调用的函数就是回调函数。(https://www.zhihu.com/question/19801131)

    • 其实就是传一段代码给某个方法A,然后方法A可以按照自己的需要在适当的时候执行这段传进来的代码。所有的回调应该都是这么个逻辑。(http://www.cnblogs.com/heshuchao/p/5376298.html

    • 编程上来说,一般使用一个库或类时,是你主动调用人家的API,这个叫Call,有的时候这样不能满足需要,需要你注册(注入)你自己的程序(比如一个对象),然后让人家在合适的时候来调用你,这叫Callback。设计模式中的Observer就是例子(http://blog.csdn.net/yu422560654/article/details/7001797

    为什么在闭包的概念里总是提到回调,这是因为Java的闭包中往往要将内部类的引用返回,如 Bar getBar() :

    public class Foo {   
        //成员变量
        private int local = 0;
        //内部类
        class Bar {
            public int func() {
                local++;
                System.out.println(local);
                return local;
            }
        }
        //返回一个内部类的引用
        public Bar getBar() {
            return new Bar();
        }
    }

    内部类的引用交管给别人,由别人在适当的时候调用,这不就是“回调”了嘛。

    看一个小小的例子,下例用于打印输出某个方法执行的耗时,通过定义接口的方式实现回调:

    public interface Callback {
        //执行回调
        void execute();
    }
    public class Tool {
    
        /**
         * 测试方法执行的耗时
         *
         * @param callback 回调方法
         */
        public static void timeConsume(Callback callback) {
            long start = System.currentTimeMillis();
            callback.execute();
            long end = System.currentTimeMillis();
            System.out.println("[time consume]:" + (end - start) + "ms");
        }
    
    }
    public class Test {
        public static void main(String[] args) {
            Tool.timeConsume(new Callback() {
                @Override
                //填写你需要测试的方法内容,这里简单写个数字计算的例子
                public void execute() {
                    int result = 0;
                    for (int i = 0; i < 100000; i++) {
                        result += i;
                    }
                    System.out.println(result);
                }
            });
        }
    }

    在Test中可以看到,直接调用,传入一个匿名内部类实现方法来完成回调,这实际上和JS中传入函数作为变量已经很相似了。你可能要说,JS中传入的函数变量可以是闭包,那么在Java中也很简单,在某个类中写好固定的内部类并写个返回内部类引用的方法,在此处调用timeConsume()时将该引用传入,就和JS中传入函数变量的形式相同了。

    2、面向接口回调

    另外,还有一点需要提醒的是,在诸多回调的使用中,都是采用的面向接口编程,让某个类实现该接口,然后传入该接口实现类。那么问题来了,为什么不直接传入对象本身的引用把自己完全暴露给别人,太不安全

    假设现在有类Boss,领导有查看所有人工资viewAllSalary,发工资paySalary等;还有一个员工类Employee。好了,现在Boss交代给Employee某件事,要求其完成之后报告给老板,这就是回调了:

    • 如果是面向接口编程,老板要实现TellMeInfo接口,然后实现接口中doThingsWithInfo
    • 回调,那得把自己的引用给员工才行,那么以TellMeInfo的实现类的形式给员工就行了
    • 员工拿到了Boss的引用,但是因为是面向接口,所以只能执行doThingsWithInfo方法

    • 如果我们直接传入对象本身的引用,老板直接写好某个方法doThingsWithInfo
    • Boss要求员工完成工作后,调用这个doThingsWithInfo方法
    • 员工拿到的是对象本身的引用,拿到一看,卧槽,惊呆了,可做的事情太多了
    • 有了这个完整引用,不就可以调用ViewAllSalary查看其他同事的薪资,甚至还能paySalary给自己多发钱
    • 员工富裕了,老板的公司倒闭了,老板没弄明白自己错在哪里

    下面来看上面场景的模拟代码,先看面向接口编程:

    //回调接口
    public interface TellMeInfo {
        void doThingsWithInfo(String result);
    }
    //领导
    public class Boss implements TellMeInfo{
    
        public void viewAllSalary() {
            //输出所有人的工资表
        }
    
        public void paySalary(Employee employee, long salary) {
            //给某员工发放薪水
        }
    
        @Override
        public void doThingsWithInfo(String result) {
            System.out.println("boss do other things according to the result:" + result);
        }
    }
    //员工
    public class Employee {
        public String work() {
            String result = "balabala";
            return result;
        }
    
        public void workAndCallback(TellMeInfo boss) {
            String result = work();
            boss.doThingsWithInfo(result);
        }
    }
    //测试类:领导让员工做完某事后报告给他,然后他才能根据事情结果去处理其他事情
    public class Test {
        public static void main(String[] args) {
            Boss boss = new Boss();
            Employee employee = new Employee();
            employee.workAndCallback(boss);
        }
    }

    那么现在看下如果直接把完整引用给员工:

    //领导
    public class Boss {
    
        public void viewAllSalary() {
            //输出所有人的工资表
        }
    
        public void paySalary(Employee employee, long salary) {
            //给某员工发放薪水
        }
    
        public void doThingsWithInfo(String result) {
            System.out.println("boss do other things according to the result:" + result);
        }
    }
    //员工
    public class Employee {
        public String work() {
            String result = "balabala";
            return result;
        }
    
        public void workAndCallback(Boss boss) {
            String result = work();
            boss.doThingsWithInfo(result);
            //好像还可以利用这个引用做点其他的事情
            //先看下其他同事的工资,哇,情敌小明的工资竟然这么高,不开心
            boss.viewAllSalary();
            //没办法,赶紧给自己多发点钱,这样可以甩小明好几条街,开心
            boss.paySalary(this, 999999);
        }
    }
    //测试类不变,老板没看出什么端倪
    public class Test {
        public static void main(String[] args) {
            Boss boss = new Boss();
            Employee employee = new Employee();
            employee.workAndCallback(boss);
        }
    }

    2、回调的方式

    • 同步回调,即阻塞,调用方要等待对方执行完成才返回
    • 异步回调,即通过异步消息进行通知
    • 回调,即双向(类似两个齿轮的咬合),“被调用的接口”被调用时也会调用“对方的接口”

    实际上我们用得最多的,还是异步回调。

    2.1 同步回调

    张老头准备泡茶喝,泡茶之前要先烧水。张老头把灶台点上火,把水壶放上,然后盯着水壶一直等,水开了,张老头用烧开的水,开心地泡起了茶。然后张老头喝好茶,就开始看书了。
    public interface Callback {
        void execute();
    }
    public class Elder implements Callback{
    
        private String name;
    
        public Elder(String name) {
            this.name = name;
        }
    
        public void readBook() {
            System.out.println(this.name + " is reading a book.");
        }
    
        public void drinkTea() {
            System.out.println(this.name + " can drink the tea right now.");
        }
    
        @Override
        public void execute() {
            drinkTea();
        }
    
    }
    public class Kettle {
    
        public void boilWater(final Callback callback) {
            System.out.println("Boiling start");
            int time = 0;
            for (int i = 0; i < 60 * 10; i++) {
                time += 1000;
            }
            System.out.println("Boiling the water costs " + time + "ms.");
            System.out.println("The water is boiling.");
            callback.execute();
        }
    
    }
    public class Test {
        public static void main(String[] args) {
            Elder elder = new Elder("Zhang");
            Kettle kettle = new Kettle();
            kettle.boilWater(elder);
            elder.readBook();
        }
    }
    
    //输出
    Boiling start
    Boiling the water costs 600000ms.
    The water is boiling.
    Zhang can drink the tea right now.
    Zhang is reading a book.

    2.2 异步回调

    还是张老头烧水喝茶的例子,他发现自己傻等着水开有点不明智,烧水可要10min呢,完全可以在这段时间先去看会儿书。等水烧开了水壶响了,再去泡茶喝,时间就利用起来了。(其他类都不变,Kettle类的boilWater方法作为线程开启,即异步
    public interface Callback {
        void execute();
    }
    public class Elder implements Callback{
    
        private String name;
    
        public Elder(String name) {
            this.name = name;
        }
    
        public void readBook() {
            System.out.println(this.name + " is reading a book.");
        }
    
        public void drinkTea() {
            System.out.println(this.name + " can drink the tea right now.");
        }
    
        @Override
        public void execute() {
            drinkTea();
        }
    
    }
    public class Kettle {
    
        public void boilWater(final Callback callback) {
            System.out.println("Boiling start");
            //开启线程,异步烧水
            new Thread(new Runnable() {
                @Override
                public void run() {
                    int time = 0;
                    for (int i = 0; i < 60 * 10; i++) {
                        time += 1000;
                    }
                    System.out.println("Boiling the water costs " + time + "ms.");
                    System.out.println("The water is boiling.");
                    callback.execute();
                }
            }).start();
        }
    
    }
    public class Test {
        public static void main(String[] args) {
            Elder elder = new Elder("Zhang");
            Kettle kettle = new Kettle();
            kettle.boilWater(elder);
            elder.readBook();
        }
    }
    
    //输出
    Boiling start
    Zhang is reading a book.
    Boiling the water costs 600000ms.
    The water is boiling.
    Zhang can drink the tea right now.

    2.3 回调(双向)

    “被调用的接口”被调用时也会调用“对方的接口”,这种情况就不适合我们的张老头出场了,双向回调多用于反复依赖对方的数据进行运算的时候,A系统要调用B系统的某个方法b(),但是这个b()方法中某个参数又需要A系统提供,于是需要反过来再调用A系统的某个方法a()提供参数,才能完整执行b()。

    那么我为什么不在A系统运算好了参数,在调用B系统的b()方法时候直接以方法参数的形式传递呢因为你不知道b()中如何使用这个参数,或者说根据条件不同甚至不会使用到这个参数

    如果这个参数的运算比较消耗资源,你不论对方使用与否都先弄出来,一股脑子塞给对方。这跟对方需要用到参数的时候,再调用你进行计算,哪个更节约资源呢?答案显而易见了。

    这就跟工厂出货一样,不管市场卖不卖得掉,先生产出来,万一市场没有需求,压根没人买,这批货就烂掉了。但是如果是市场给工厂发了需求订单,工厂再进行相应生产,再出货,那效果就截然不同了。

    看一个简单的例子,随机生成某个随机百分比的字符串(A实例调用了B中某个方法,而这个方法需要数据又反过来又调用了A中某个方法):
    public interface Callback {
        public double takeRandom();
    }
    public class A implements Callback{
    
        private B b = new B();
    
        @Override
        public double takeRandom() {
            System.out.println(this + " executing the method takeRandom()");
            return Math.random();
        }
    
        public void printRandomPercent() {
            System.out.println(this + " executing the method printRandomPercent()");
            System.out.println("start");
            //A类实例的函数调用B类实例的方法
            b.doPercent(this);
        }
    
    }
    public class B {
        
        public void doPercent(Callback action) {
            System.out.println(this + " executing the method doPercent()");
            double param = action.takeRandom();
            DecimalFormat decimalFormat = new DecimalFormat("0.00");
            String result = decimalFormat.format(param * 100) + "%";
            System.out.println("the calculate-result is " + result);
        }
        
    }
    public class Test {
        public static void main(String[] args) {
            A a = new A();
            a.printRandomPercent();
        }
    }
    
    //输出
    callback.A@186db54 executing the method printRandomPercent()
    start
    callback.B@a97b0b executing the method doPercent()
    callback.A@186db54 executing the method takeRandom()
    the calculate-result is 7.43%

    3、参考链接



  • 相关阅读:
    springboot运行时该注意的地方
    建立第一个SpringBoot小列子(碰到的错误)
    mysql建表基本语法
    两层c:forEach循环嵌套
    git-------基础知识(本地推送项目版本---github上)
    《生活的邀请函》___ 奥雷阿
    输入路径(包括盘符),打印层级目录(隐藏文件也会打印出来)
    输入两个文件夹路径,把其中一个文件夹中(包含内容)拷贝到另一个文件夹中
    用java实现歌曲串烧高速版(SequenceInputStream的使用)
    Java中InputStream装饰器模式的大家族
  • 原文地址:https://www.cnblogs.com/deng-cc/p/8241894.html
Copyright © 2011-2022 走看看