首頁  >  文章  >  Java  >  Java回呼方法詳解

Java回呼方法詳解

高洛峰
高洛峰原創
2017-01-24 11:46:331550瀏覽

回呼在維基百科中定義為:

在電腦程式設計中,回呼函數,是指透過函數參數傳遞到其他程式碼的,某一塊可執行程式碼的引用。

其目的是允許底層程式碼呼叫在高層定義的子程式。

舉個例子可能更明白一些:以Android中用retrofit進行網路請求為例,這是非同步回呼的一個例子。

在發起網路請求之後,app可以繼續其他事情,網路請求的結果一般是透過onResponse與onFailure這兩個方法回傳而得到。看一下相關部分的程式碼:

call.enqueue(new Callback<HistoryBean>() {
      @Override
      public void onResponse(Call<HistoryBean> call, Response<HistoryBean> response) {
        HistoryBean hb = response.body();
        if(hb == null) return;
        showText.append(hb.isError() + "");
        for(HistoryBean.ResultsBean rb : hb.getResults()){
          showText.append(rb.getTitle() + "/n");
        }
      }
      @Override
      public void onFailure(Call<HistoryBean> call, Throwable t) {
      }
    });

忽略上面CallBack中的泛型,依照維基百科中的定義,匿名內部類別裡面的全部程式碼可以看成函數參數傳遞到其他程式碼的,某一塊可執行程式碼的引用。 onResponse與onFailure這兩個方法就是回呼方法。底層的程式碼就是已經寫好不變的網路請求部分,高層定義的子程式就是回調,因為具體的實作交給了使用者,所以具備了很高的彈性。上面就是透過enqueue(Callback callback)這個方法來關聯起來的。

回呼方法的步驟

上面說的回調是很通用的概念,放到程式書寫上面,就可以說:

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類別中呼叫B的方法f(CallBack callback)-註冊回呼介面

B就可以在f(CallBack callback)方法中呼叫A的方法-呼叫回呼介面

回調的例子

我們以一個兒子在玩遊戲,等媽媽把飯做好在通知兒子來吃為例,按照上面的步驟去寫回調;

上面的例子中,顯然應該兒子來實現回調接口,母親調用回調接口。所以我們先定義一個回呼介面,然後讓兒子去實現這個回呼介面。

其程式碼如下:

public interface CallBack {
  void eat();
}
public class Son implements CallBack{
  private Mom mom;
  //A类持有对B类的引用
  public void setMom(Mom mom){
    this.mom = mom;
  }
  @Override
  public void eat() {
    System.out.println("我来吃饭了");
  }
  public void askMom(){
    //通过B类的引用调用含有接口参数的方法。
     System.out.println("饭做了吗?");
    System.out.println("没做好,我玩游戏了");
    new Thread(() -> mom.doCook(Son.this)).start();
    System.out.println("玩游戏了中......");
  }
}

接著我們還要定義一個母親的類,裡面有一個含有介面參數的方法doCook

public class Mom {
  //在含有接口参数的方法中利用接口参数调用回调方法
  public void doCook(CallBack callBack){
    new Thread(new Runnable() {
      @Override
      public void run() {
        try {
          System.out.println("做饭中......");
          Thread.sleep(5000);
          callBack.eat();
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
    }).start();
  }
}

   

的回調的例子。 Son類別實作了介面的回呼方法,透過askMom這個方法呼叫Mom類別中的doCook,實作註冊回調接口,相當於A類別中呼叫B類別的程式碼C。在Mom類別中的doCook來回調Son類別中的eat,來告訴Son類別中的結果。

這樣,我們實現了一個簡單的,符合定義的回調。

回呼範例的進一步探索

我們主要看一下Son類別的程式碼:

public class Test {
  public static void main(String[] args) {
    Mom mom = new Mom();
    Son son = new Son();
    son.setMom(mom);
    son.askMom();
  }
}

   

這個類別裡面,除了輸出一些語句之外,真正有用的部分是mom.doCook(S這個類別)重寫eat方法。所以,我們可以透過匿名內部類別的形式,簡寫這個回呼。其程式碼如下:

public class Son implements CallBack{
  public Mom mom;
  public Son(Mom mom){
    this.mom = mom;
  }
  public void askMom(){
    System.out.println("饭做了吗?");
    System.out.println("没做好,我玩游戏了");
    new Thread(() -> mom.doCook(Son.this)).start();
    System.out.println("玩游戏了中......");
  }
  @Override
  public void eat() {
    System.out.println("好了,我来吃饭了");
  }
}

取消Son類,直接在主方法中透過匿名內部類別去實作eat方法。其實匿名內部類別就是回調的體現。

異步回調與同步回調

回調上面我們講了 就是A調用B類中的方法C,然後在方法C裡面通過A類的對象去調用A類中的方法D。

我們在說一下非同步與同步,先說同步的概念

同步

同步指的是在呼叫方法的時候,如果上一個方法呼叫沒有執行完,是無法進行新的方法呼叫。也就是說事情必須一件事情一件事情的做,做完上一件,才能做下一件。

非同步

非同步相對於同步,可以不需要等上個方法呼叫結束,才呼叫新的方法。所以,在非同步的方法呼叫中,是需要一個方法來通知使用者方法呼叫結果的。

實現非同步的方式

在Java中最常实现的异步方式就是让你想异步的方法在一个新线程中执行。

我们会发现一点,异步方法调用中需要一个方法来通知使用者调用结果,结合上面所讲,我们会发现回调方法就适合做这个事情,通过回调方法来通知使用者调用的结果。

那异步回调就是A调用B的方法C时是在一个新线程当中去做的。

上面的母亲通知儿子吃饭的例子,就是一个异步回调的例子。在一个新线程中,调用doCook方法,最后通过eat来接受返回值,当然使用lamdba优化之后的,本质是一样的。

同步回调就是A调用B的方法C没有在一个新线程,在执行这个方法C的时候,我们什么都不能做,只能等待他执行完成。

同步回调与异步回调的例子

我们看一个Android中的一个同步回调的例子:

button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
       Log.i("button","被点击");
    }
});

button通过setOnClickListener注册回调函数,和上面写的一样,通过匿名内部类的形式将接口的引用传进去。由于button调用setOnClickListener没有新建一个线程,所以这个是同步的回调。

而异步回调,就是我们开篇讲的那个例子:

call.enqueue(new Callback<HistoryBean>() {
      @Override
      public void onResponse(Call<HistoryBean> call, Response<HistoryBean> response) {
        HistoryBean hb = response.body();
        if(hb == null) return;
        showText.append(hb.isError() + "");
        for(HistoryBean.ResultsBean rb : hb.getResults()){
          showText.append(rb.getTitle() + "/n");
        }
      }
      @Override
      public void onFailure(Call<HistoryBean> call, Throwable t) {
      }
    });

   

这个enqueue方法是一个异步方法去请求远程的网络数据。其内部实现的时候是通过一个新线程去执行的。

通过这两个例子,我们可以看出同步回调与异步回调的使用其实是根据不同的需求而设计。不能说一种取代另一种,像上面的按钮点击事件中,如果是异步回调,用户点击按钮之后其点击效果不是马上出现,而用户又不会执行其他操作,那么会感觉很奇怪。而像网络请求的异步回调,因为受限于请求资源可能不存在,网络连接不稳定等等原因导致用户不清楚方法执行的时候,所以会用异步回调,发起方法调用之后去做其他事情,然后等回调的通知。

回调方法在通信中的应用

上面提到的回调方法,除了网络请求框架的回调除外,其回调方法都是没有参数,下面,我们看一下在回调方法中加入参数来实现一些通信问题。

如果我们想要A类得到B类经过一系列计算,处理后数据,而且两个类是不能通过简单的将B的引用给A类就可以得到数据的。我们可以考虑回调。

步骤如下:

在拥有数据的那个类里面写一个回调的接口。-->这里就是B类中写一个回调接口

回调方法接收一个参数,这个参数就是要得到的数据

同样是在这个类里写一个注册回调的方法。

在注册回调方法,用接口的引用去调用回调接口,把B类的数据当做参数传入回调的方法中。

在A类中,用B类的引用去注册回调接口,把B类中的数据通过回调传到A类中。

上面说的步骤,有点抽象。下面我们看一个例子,一个是Client,一个是Server。Client去请求Server经过耗时处理后的数据。

public class Client{
  public Server server;
  public String request;
  //链接Server,得到Server引用。
  public Client connect(Server server){
    this.server = server;
    return this;
  }
  //Client,设置request
  public Client setRequest(String request){
    this.request = request;
    return this;
  }
  //异步发送请求的方法,lamdba表达式。
  public void enqueue(Server.CallBack callBack){
    new Thread(()->server.setCallBack(request,callBack)).start();
  }
}
public class Server {
  public String response = "这是一个html";
  //注册回调接口的方法,把数据通过参数传给回调接口
  public void setCallBack(String request,CallBack callBack){
    System.out.println("已经收到request,正在计算当中......");
    new Thread(() -> {
      try {
        Thread.sleep(5000);
        callBack.onResponse(request + response);
      } catch (InterruptedException e) {
        e.printStackTrace();
        callBack.onFail(e);
      }
    }).start();
  }
  //在拥有数据的那个类里面写一个接口
  public interface CallBack{
    void onResponse(String response);
    void onFail(Throwable throwable);
  }
}

   

接下来,我们看一下测试的例子:

public class CallBackTest {
  public static void main(String[] args) {
    Client client = new Client();
    client.connect(new Server()).setRequest("这个文件是什么?").enqueue(new Server.CallBack() {
      @Override
      public void onResponse(String response) {
        System.out.println(response);
      }
      @Override
      public void onFail(Throwable throwable) {
        System.out.println(throwable.getMessage());
      }
    });
  }
}

   

结果如下:

已经收到request,正在计算当中......
这个文件是什么?这是一个html

  

以上就是通过回调的方式进行通信。

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,同时也希望多多支持PHP中文网!

更多Java回调方法详解相关文章请关注PHP中文网!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn