夯實Java基礎系列11:深刻理解Java中的回調機制

本系列文章將整理到我在GitHub上的《Java面試指南》倉庫,更多精彩內容請到個人倉庫裏查看html

https://github.com/h2pl/Java-Tutorialjava

喜歡的話麻煩點下Star哈git

文章首發於個人我的博客:程序員

www.how2playlife.comgithub

本文是微信公衆號【Java技術江湖】的《夯實Java基礎系列博文》其中一篇,本文部份內容來源於網絡,爲了把本文主題講得清晰透徹,也整合了不少我認爲不錯的技術博客內容,引用其中了一些比較好的博客文章,若有侵權,請聯繫做者。該系列博文會告訴你如何從入門到進階,一步步地學習Java基礎知識,並上手進行實戰,接着瞭解每一個Java知識點背後的實現原理,更完整地瞭解整個Java技術體系,造成本身的知識框架。爲了更好地總結和檢驗你的學習成果,本系列文章也會提供每一個知識點對應的面試題以及參考答案。web

若是對本系列文章有什麼建議,或者是有什麼疑問的話,也能夠關注公衆號【Java技術江湖】聯繫做者,歡迎你參與本系列博文的創做和修訂。面試

@[toc] 算法

模塊間的調用

本部分摘自https://www.cnblogs.com/xrq730/p/6424471.html後端

在一個應用系統中,不管使用何種語言開發,必然存在模塊之間的調用,調用的方式分爲幾種:緩存

(1)同步調用

同步調用是最基本而且最簡單的一種調用方式,類A的方法a()調用類B的方法b(),一直等待b()方法執行完畢,a()方法繼續往下走。這種調用方式適用於方法b()執行時間不長的狀況,由於b()方法執行時間一長或者直接阻塞的話,a()方法的餘下代碼是沒法執行下去的,這樣會形成整個流程的阻塞。

(2)異步調用

異步調用是爲了解決同步調用可能出現阻塞,致使整個流程卡住而產生的一種調用方式。類A的方法方法a()經過新起線程的方式調用類B的方法b(),代碼接着直接往下執行,這樣不管方法b()執行時間多久,都不會阻塞住方法a()的執行。

可是這種方式,因爲方法a()不等待方法b()的執行完成,在方法a()須要方法b()執行結果的狀況下(視具體業務而定,有些業務好比啓異步線程發個微信通知、刷新一個緩存這種就不必),必須經過必定的方式對方法b()的執行結果進行監聽。

在Java中,可使用Future+Callable的方式作到這一點,具體作法能夠參見個人這篇文章Java多線程21:多線程下其餘組件之CyclicBarrier、Callable、Future和FutureTask。

(3)回調

一、什麼是回調?通常來講,模塊之間都存在必定的調用關係,從調用方式上看,能夠分爲三類同步調用、異步調用和回調。同步調用是一種阻塞式調用,即在函數A的函數體裏經過書寫函數B的函數名來調用之,使內存中對應函數B的代碼得以執行。異步調用是一種相似消息或事件的機制解決了同步阻塞的問題,例如 A通知 B後,他們各走各的路,互不影響,不用像同步調用那樣, A通知 B後,非得等到 B走完後, A才繼續走 。回調是一種雙向的調用模式,也就是說,被調用的接口被調用時也會調用對方的接口,例如A要調用B,B在執行完又要調用A。

二、回調的用途回調通常用於層間協做,上層將本層函數安裝在下層,這個函數就是回調,而下層在必定條件下觸發回調。例如做爲一個驅動,是一個底層,他在收到一個數據時,除了完成本層的處理工做外,還將進行回調,將這個數據交給上層應用層來作進一步處理,這在分層的數據通訊中很廣泛。

多線程中的「回調」

Java多線程中能夠經過callable和future或futuretask結合來獲取線程執行後的返回值。實現方法是經過get方法來調用callable的call方法獲取返回值。

其實這種方法本質上不是回調,回調要求的是任務完成之後被調用者主動回調調用者的接口。而這裏是調用者主動使用get方法阻塞獲取返回值。

public class 多線程中的回調 {
    //這裏簡單地使用future和callable實現了線程執行完後
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executor = Executors.newCachedThreadPool();
        Future<String> future = executor.submit(new Callable<String>() {
            @Override
            public String call() throws Exception {
                System.out.println("call");
                TimeUnit.SECONDS.sleep(1);
                return "str";
            }
        });
        //手動阻塞調用get經過call方法得到返回值。
        System.out.println(future.get());
        //須要手動關閉,否則線程池的線程會繼續執行。
        executor.shutdown();

    //使用futuretask同時做爲線程執行單元和數據請求單元。
    FutureTask<Integer> futureTask = new FutureTask(new Callable<Integer>() {
        @Override
        public Integer call() throws Exception {
            System.out.println("dasds");
            return new Random().nextInt();
        }
    });
    new Thread(futureTask).start();
    //阻塞獲取返回值
    System.out.println(futureTask.get());
}
@Test
public void test () {
    Callable callable = new Callable() {
        @Override
        public Object call() throws Exception {
            return null;
        }
    };
    FutureTask futureTask = new FutureTask(callable);

}
}複製代碼

Java回調機制實戰

曾經本身偶爾據說過回調機制,隱隱約約可以懂一些意思,可是當讓本身寫一個簡單的示例程序時,本身就傻眼了。隨着工做經驗的增長,本身常常聽到這兒使用了回調,那兒使用了回調,本身是時候好好研究一下Java回調機制了。網上關於Java回調的文章一抓一大把,可是看完老是雲裏霧裏,不知所云,特別是看到抓取別人的代碼走兩步時,老是現眼。因而本身決定寫一篇關於Java機制的文章,以方便你們和本身更深刻的學習Java回調機制。

首先,什麼是回調函數,引用百度百科的解釋:回調函數就是一個經過函數指針調用的函數。若是你把函數的指針(地址)做爲參數傳遞給另外一個函數,當這個指針被用來調用其所指向的函數時,咱們就說這是回調函數。回調函數不是由該函數的實現方直接調用,而是在特定的事件或條件發生時由另外的一方調用的,用於對該事件或條件進行響應[2].

很差意思,上述解釋我看了好幾遍,也沒理解其中深入奧祕,相信一些讀者你也同樣。光說不練假把式,我們仍是以實戰理解脈絡。

實例一 : 同步調用

本文以底層服務BottomService和上層服務UpperService爲示例,利用上層服務調用底層服務,總體執行過程以下:

第一步: 執行UpperService.callBottomService();

第二步: 執行BottomService.bottom();

第三步:執行UpperService.upperTaskAfterCallBottomService()

1.1 同步調用代碼

同步調用時序圖:

[外鏈圖片轉存失敗(img-dapFATDy-1569148364574)(https://upload-images.jianshu.io/upload_images/3796264-6a5b5b898aa3930e.png?imageMogr2/auto-orient/strip|imageView2/2/w/1031/format/webp)]

同步調用時序圖

1.1.1 底層服務類:BottomService.java

package synchronization.demo;

/**

* Created by lance on 2017/1/19.

*/

public class BottomService {

public String bottom(String param) {

try { //  模擬底層處理耗時,上層服務須要等待

Thread.sleep(3000);

} catch (InterruptedException e) {

e.printStackTrace();

}

return param +" BottomService.bottom() execute -->";

}

}
複製代碼

1.1.2 上層服務接口: UpperService.java

package synchronization.demo;

/**

* Created by lance on 2017/1/19.

*/

public interface UpperService {

public void upperTaskAfterCallBottomService(String upperParam);

public String callBottomService(final String param);

}
複製代碼

1.1.3 上層服務接口實現類:UpperServiceImpl.java

package synchronization.demo;

/**

* Created by lance on 2017/1/19.

*/

public class UpperServiceImpl implements UpperService {

private BottomService bottomService;

@Override

public void upperTaskAfterCallBottomService(String upperParam) {

System.out.println(upperParam + " upperTaskAfterCallBottomService() execute.");

}

public UpperServiceImpl(BottomService bottomService) {

this.bottomService = bottomService;

}

@Override

public String callBottomService(final String param) {

return bottomService.bottom(param + " callBottomService.bottom() execute --> ");

}

}
複製代碼

1.1.4 Test測試類:Test.java

package synchronization.demo;

import java.util.Date;

/**

* Created by lance on 2017/1/19.

*/

public class Test {

public static void main(String[] args) {

BottomService bottomService = new BottomService();

UpperService upperService = new UpperServiceImpl(bottomService);

System.out.println("=============== callBottomService start ==================:" + new Date());

String result = upperService.callBottomService("callBottomService start --> ");

//upperTaskAfterCallBottomService執行必須等待callBottomService()調用BottomService.bottom()方法返回後纔可以執行

upperService.upperTaskAfterCallBottomService(result);

System.out.println("=============== callBottomService end ====================:" + new Date());

}

}
複製代碼

1.1.5 輸出結果:

=============== callBottomService start ==================:Thu Jan 19 14:59:58 CST 2017

callBottomService start -->  callBottomService.bottom() execute -->  BottomService.bottom() execute --> upperTaskAfterCallBottomService() execute.

=============== callBottomService end ====================:Thu Jan 19 15:00:01 CST 2017
複製代碼

注意輸出結果:

是同步方式,Test調用callBottomService()等待執行結束,而後再執行下一步,即執行結束。callBottomService開始執行時間爲Thu Jan 19 14:59:58 CST 2017,執行結束時間爲Thu Jan 19 15:00:01 CST 2017,耗時3秒鐘,與模擬的耗時時間一致,即3000毫秒。

實例二:由淺入深

前幾天公司面試有問道java回調的問題,由於這方面也沒有太多研究,因此回答的含糊不清,這回特地來補習一下。看了看網上的回調解釋和例子,都那麼的繞口,得看半天才能繞回來,其實吧,回調是個很簡單的機制。在這裏我用簡單的語言先來解釋一下:假設有兩個類,分別是A和B,在A中有一個方法a(),B中有一個方法b();在A裏面調用B中的方法b(),而方法b()中調用了方法a(),這樣子就同時實現了b()和a()兩個方法的功能。

疑惑:爲啥這麼麻煩,我直接在類A中的B.b()方法下調用a()方法就好了唄。解答:回調更像是一個約定,就是若是我調用了b()方法,那麼就必需要回調,而不須要顯示調用1、Java的回調-淺咱們用例子來解釋:小明和小李相約一塊兒去吃早飯,可是小李起的有點晚要先洗漱,等小李洗漱完成後,通知小明再一塊兒去吃飯。小明就是類A,小李就是類B。一塊兒去吃飯這個事件就是方法a(),小李去洗漱就是方法b()。

public class XiaoMing { 
   //小明和小李一塊兒吃飯
   public void eatFood() {
      XiaoLi xl = new XiaoLi();
      //A調用B的方法
      xl.washFace();
   }
 
   public void eat() {
      System.out.print("小明和小李一塊兒去吃大龍蝦");
   }
}
那麼怎麼讓小李洗漱完後在通知小明一塊兒去吃飯呢

public class XiaoMing { 
   //小明和小李一塊兒吃飯
   public void eatFood() {
      XiaoLi xl = new XiaoLi();
      //A調用B的方法
      xl.washFace();
      eat();
   }
 
   public void eat() {
      System.out.print("小明和小李一塊兒去吃大龍蝦");
   }
}複製代碼

不過上面已經說過了這個不是回調函數,因此不能這樣子,正確的方式以下

public class XiaoLi{//小李
   public void washFace() {
    System.out.print("小李要洗漱");
    XiaoMing xm = new XiaoMing();
        //B調用A的方法
    xm.eat();//洗漱完後,一塊兒去吃飯
   }
}複製代碼

這樣子就能夠實現washFace()同時也能實現eat()。小李洗漱完後,再通知小明一塊兒去吃飯,這就是回調。

2、Java的回調-中但是細心的夥伴可能會發現,小李的代碼徹底寫死了,這樣子的場合可能適用和小明一塊兒去吃飯,但是假如小李洗漱完不吃飯了,想和小王上網去,這樣子就不適用了。其實上面是僞代碼,僅僅是幫助你們理解的,真正狀況下是須要利用接口來設置回調的。如今咱們繼續用小明和小李去吃飯的例子來說講接口是如何使用的。

小明和小李相約一塊兒去吃早飯,可是小李起的有點晚要先洗漱,等小李洗漱完成後,通知小明再一塊兒去吃飯。小明就是類A,小李就是類B。不一樣的是咱們新建一個吃飯的接口EatRice,接口中有個抽象方法eat()。在小明中調用這個接口,並實現eat();小李聲明這個接口對象,而且調用這個接口的抽象方法。這裏可能有點繞口,不過不要緊,看看例子就很清楚了。

EatRice接口:

public interface EatRice {
   public void eat(String food);
}
小明:

public class XiaoMing implements EatRice{//小明
    
   //小明和小李一塊兒吃飯
   public void eatFood() {
    XiaoLi xl = new XiaoLi();
    //A調用B的方法
    xl.washFace("大龍蝦", this);//this指的是小明這個類實現的EatRice接口
   }
 
   @Override
   public void eat(String food) {
    // TODO Auto-generated method stub
    System.out.println("小明和小李一塊兒去吃" + food);
   }
}
小李:

public class XiaoLi{//小李
   public void washFace(String food,EatRice er) {
    System.out.println("小李要洗漱");
        //B調用了A的方法
    er.eat(food);
   }
}
測試Demo:

public class demo {
   public static void main(String args[]) {
    XiaoMing xm = new XiaoMing();
    xm.eatFood();
   }
}複製代碼

測試結果:

這樣子就經過接口的形式實現了軟編碼。經過接口的形式我能夠實現小李洗漱完後,和小王一塊兒去上網。代碼以下

public class XiaoWang implements EatRice{//小王
    
   //小王和小李一塊兒去上網
   public void eatFood() {
    XiaoLi xl = new XiaoLi();
    //A調用B的方法
    xl.washFace("輕舞飛揚上網", this);
   }
 
   @Override
   public void eat(String bar) {
    // TODO Auto-generated method stub
    System.out.println("小王和小李一塊兒去" + bar);
   }
}複製代碼

實例三:Tom作題

數學老師讓Tom作一道題,而且Tom作題期間數學老師不用盯着Tom,而是在玩手機,等Tom把題目作完後再把答案告訴老師。

1 數學老師須要Tom的一個引用,而後才能將題目發給Tom。

2 數學老師須要提供一個方法以便Tom作完題目之後可以將答案告訴他。

3 Tom須要數學老師的一個引用,以便Tom把答案給這位老師,而不是隔壁的體育老師。

回調接口,能夠理解爲老師接口

//回調指的是A調用B來作一件事,B作完之後將結果告訴給A,這期間A能夠作別的事情。
    //這個接口中有一個方法,意爲B作完題目後告訴A時使用的方法。
    //因此咱們必須提供這個接口以便讓B來回調。
    //回調接口,
    public interface CallBack {
        void tellAnswer(int res);
    }複製代碼


數學老師類​

//老師類實例化回調接口,即學生寫完題目以後經過老師的提供的方法進行回調。
    //那麼學生如何調用到老師的方法呢,只要在學生類的方法中傳入老師的引用便可。
    //而老師須要指定學生答題,因此也要傳入學生的實例。
public class Teacher implements CallBack{
    private Student student;

    Teacher(Student student) {
        this.student = student;
    }

    void askProblem (Student student, Teacher teacher) {
        //main方法是主線程運行,爲了實現異步回調,這裏開啓一個線程來操做
        new Thread(new Runnable() {
            @Override
            public void run() {
                student.resolveProblem(teacher);
            }
        }).start();
        //老師讓學生作題之後,等待學生回答的這段時間,能夠作別的事,好比玩手機.\
        //而不須要同步等待,這就是回調的好處。
        //固然你能夠說開啓一個線程讓學生作題就好了,可是這樣沒法讓學生通知老師。
        //須要另外的機制去實現通知過程。
        // 固然,多線程中的future和callable也能夠實現數據獲取的功能。
        for (int i = 1;i < 4;i ++) {
            System.out.println("等學生回答問題的時候老師玩了 " + i + "秒的手機");
        }
    }

    @Override
    public void tellAnswer(int res) {
        System.out.println("the answer is " + res);
    }
}複製代碼

學生接口

//學生的接口,解決問題的方法中要傳入老師的引用,不然沒法完成對具體實例的回調。
    //寫爲接口的好處就是,不少個學生均可以實現這個接口,而且老師在提問題時能夠經過
    //傳入List<Student>來聚合學生,十分方便。
public interface Student {
    void resolveProblem (Teacher teacher);
}複製代碼

學生Tom

public class Tom implements Student{

    @Override
    public void resolveProblem(Teacher teacher) {
        try {
            //學生思考了3秒後獲得了答案,經過老師提供的回調方法告訴老師。
            Thread.sleep(3000);
            System.out.println("work out");
            teacher.tellAnswer(111);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }複製代碼

測試類

public class Test {
    public static void main(String[] args) {
        //測試
        Student tom = new Tom();
        Teacher lee = new Teacher(tom);
        lee.askProblem(tom, lee);
        //結果
//        等學生回答問題的時候老師玩了 1秒的手機
//        等學生回答問題的時候老師玩了 2秒的手機
//        等學生回答問題的時候老師玩了 3秒的手機
//        work out
//        the answer is 111
    }
}複製代碼

參考文章

https://blog.csdn.net/fengye454545/article/details/80198446https://blog.csdn.net/xiaanming/article/details/8703708/https://www.cnblogs.com/prayjourney/p/9667835.htmlhttps://blog.csdn.net/qq_25652949/article/details/86572948https://my.oschina.net/u/3703858/blog/1798627

微信公衆號

Java技術江湖

若是你們想要實時關注我更新的文章以及分享的乾貨的話,能夠關注個人公衆號【Java技術江湖】一位阿里 Java 工程師的技術小站,做者黃小斜,專一 Java 相關技術:SSM、SpringBoot、MySQL、分佈式、中間件、集羣、Linux、網絡、多線程,偶爾講點Docker、ELK,同時也分享技術乾貨和學習經驗,致力於Java全棧開發!

Java工程師必備學習資源: 一些Java工程師經常使用學習資源,關注公衆號後,後臺回覆關鍵字 「Java」 便可免費無套路獲取。

個人公衆號

我的公衆號:黃小斜

做者是 985 碩士,螞蟻金服 JAVA 工程師,專一於 JAVA 後端技術棧:SpringBoot、MySQL、分佈式、中間件、微服務,同時也懂點投資理財,偶爾講點算法和計算機理論基礎,堅持學習和寫做,相信終身學習的力量!

程序員3T技術學習資源: 一些程序員學習技術的資源大禮包,關注公衆號後,後臺回覆關鍵字 「資料」 便可免費無套路獲取。

相關文章
相關標籤/搜索