Java回调方法详解
回调在维基百科中定义为:
在计算机程序设计中,回调函数,是指通过函数参数传递到其他代码的,某一块可执行代码的引用。
其目的是允许底层代码调用在高层定义的子程序。
举个例子可能更明白一些:以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类实现接口CallBack callback
- A类中包含了一个B的引用
- B中有一个参数为CallBack的方法f(CallBack callback)
- 在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(); } }
我们通过一个测试类:
public class Test { public static void main(String[] args) { Mom mom = new Mom(); Son son = new Son(); son.setMom(mom); son.askMom(); } }
这个例子就是典型的回调的例子。Son类实现了接口的回调方法,通过askMom这个方法调用Mom类中的doCook,实现注册回调接口,相当于A类中调用B类的代码C。在Mom类中的doCook来回调Son类中的eat,来告诉Son类中的结果。
这样,我们就实现了一个简单的,符合定义的回调。
回调例子的进一步探索
我们主要看一下Son类的代码:
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("好了,我来吃饭了"); } }
这个类里面,除了输出一些语句之外,真正有用的部分是mom.doCook(Son.this)以及重写eat方法。所以,我们可以通过匿名内部类的形式,简写这个回调。其代码如下:
public class CallBackTest { public static void main(String[] args) { Mom mom = new Mom(); new Thread(()-> mom.doCook(() -> System.out.println("吃饭了......"))).start(); } }
取消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
以上就是通过回调的方式进行通信。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,同时也希望多多支持我们!