[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、参考链接



posted @ 2018-01-08 11:33  Dulk  阅读(262)  评论(0编辑  收藏  举报