深入淺出回撥函式(知乎上看到最好的回答)
前言
在Java社群的各種開源工具中,回撥方法的使用俯拾即是。所以熟悉回撥方法無疑能加速自己對開源輪子的掌握。
網上搜了一些文章,奈何對回撥方法的介紹大多隻停留在什麼是回撥方法的程度上。本篇文章嘗試從回撥方法怎麼來的、為什麼要使用回撥方法以及在實際專案中如何使用等方面來介紹下。
場景選擇的得當與否,很影響讀者的繼續閱讀的興趣甚至理解的主動性(長期作為網際網路技術博文讀者的我,深有感觸)。
好場景私以為是:熟悉且簡單。
本例小心翼翼選擇的場景是:寫作業。(hope you like)
自己寫注:寫作業這個動作至少交代三個方面:誰,什麼動作(寫),寫什麼。
下面先從(有個學生,寫,作業)開始。
# 1. 有個學生
Student student = new Student();
# 2. 該學生有寫作業這個動作需要執行
student.doHomeWork(someHomeWork);
# 3. 注意到這個寫作業這個動作是需要得到入參“作業”的後才能進行的。所以給這個學生new了個簡單的題目做。
String aHomeWork = "1+1=?";
student.doHomeWork(aHomeWork);
至此,完成寫作業的動作。
完整程式碼
public class Student { public void doHomeWork(String homeWork) { System.out.println("作業本"); if("1+1=?".equals(homeWork)) { System.out.println("作業:"+homeWork+" 答案:"+"2"); } else { System.out.println("作業:"+homeWork+" 答案:"+"不知道~~"); } } public static void main(String[] args) { Student student = new Student(); String aHomeWork = "1+1=?"; student.doHomeWork(aHomeWork); } }
程式執行
作業本
作業:1+1=? 答案:2
我們一定要把焦點聚焦到,”寫作業“這個需求上面。
該學生寫作業的方法是現成的,但是需要有作業作為入參,怎麼獲取作業才是完成動作的關鍵。希望這點能深深印入我們的腦海。
讓室友幫忙解答
上面的例子中該同學自己呼叫自己的方法,把收到的homework直接寫了。
但是現實可能會出現各種各樣的問題導致該同學不能(xiang)自己來做。比如他想玩遊戲或者有約會。所以他拜託了他的好室友(roommate)來幫忙寫下。該怎麼實現呢。
#1. 因為室友幫忙寫,所以在doHomeWork動作裡面,就不需要有邏輯判斷的程式碼,因為舍友會直接把答案寫進來。改成: student.doHomeWork(aHomeWork, theAnswer); #上句中做作業的動作支援傳入“作業”和“答案”,有了這兩個,就說明能做好作業了。 #其中aHomeWork作業是已知的,但是theAnswer這個答案卻是室友提供的。 #室友怎麼才能提供答案呢,最簡單是,室友這個物件直接提供一個傳入作業,傳出答案的方法,這樣該同學就可以直接呼叫了。 RoomMate roomMate = new RoomMate(); String theAnswer = roomMate.getAnswer(aHomeWork); student.doHomeWork(aHomeWork, theAnswer);
完整程式碼
public class Student {
public void doHomeWork(String homeWork, String answer) {
System.out.println("作業本");
if(answer != null) {
System.out.println("作業:"+homeWork+" 答案:"+ answer);
} else {
System.out.println("作業:"+homeWork+" 答案:"+ "(空白)");
}
}
public static void main(String[] args) {
Student student = new Student();
String aHomeWork = "1+1=?";
RoomMate roomMate = new RoomMate();
String theAnswer = roomMate.getAnswer(aHomeWork);
student.doHomeWork(aHomeWork, theAnswer);
}
}
public class RoomMate {
public String getAnswer(String homework) {
if("1+1=?".equals(homework)) {
return "2";
} else {
return null;
}
}
}
程式執行
作業本
作業:1+1=? 答案:2
怒,說好的回撥方法呢~~
因為到目前為止,不需要使用回撥方法。
技術總是伴隨新的需求出現的。
好,給你新的需求。
注意重點來了
我們回顧下這兩行程式碼
#室友寫好作業
String theAnswer = roomMate.getAnswer(aHomeWork);
#該同學直接抄答案,完成作業
student.doHomeWork(aHomeWork, theAnswer);
====================================================
該同學想了想,你給了答案有屁用,還得要我自己謄寫到作業本上面去(執行自己的做作業方法)。
你就不能直接呼叫我的做作業方法幫我把答案寫好,把作業做完得了。
經不住該同學的軟磨硬泡,“中國好室友”答應了。怎麼實現呢。
再回顧下做作業的全過程
#待解決的作業
String aHomeWork = "1+1=?";
#室友寫出答案
String theAnswer = roomMate.getAnswer(aHomeWork);
#該同學呼叫,自己把答案寫到作業本。(也即是這個步驟不給呼叫了)
student.doHomeWork(aHomeWork, theAnswer);
#做作業必須得呼叫這個方法,而根據需求這個方法必須由室友去呼叫。那很顯然,該室友得保持一個該同學的引用,才能正常呼叫啊。
#燈燈燈~
#室友說,那你在呼叫getAnswer方法的時候,除了傳入作業,還需要把自己的引用放裡面。這樣我做完了,直接呼叫你的做作業方法就行了。
roomMate.getAnswer(aHomeWork,student);
完整程式碼
public class Student {
public void doHomeWork(String homeWork, String answer) {
System.out.println("作業本");
if(answer != null) {
System.out.println("作業:"+homeWork+" 答案:"+ answer);
} else {
System.out.println("作業:"+homeWork+" 答案:"+ "(空白)");
}
}
public static void main(String[] args) {
Student student = new Student();
String aHomeWork = "1+1=?";
RoomMate roomMate = new RoomMate();
roomMate.getAnswer(aHomeWork,student);
}
}
public class RoomMate {
public void getAnswer(String homework, Student student) {
if("1+1=?".equals(homework)) {
student.doHomeWork(homework, "2");
} else {
student.doHomeWork(homework, "(空白)");
}
}
}
執行程式
作業本
作業:1+1=? 答案:2
回撥方法
在上述“讓室友直接把作業寫了”的例子中,其實已經體現了回撥的意思。
場景的核心在於這位學生要把作業給做了。
簡單點描述:這位學生告訴室友要做什麼作業,並把自己的引用也給了室友。該室友得到作業,做完後直接引用該學生並呼叫其做作業的方法,完成代寫作業的任務。
稍微複雜點描述:
該學生做作業的方法有兩個入參,一個是作業題目(已知),一個是作業答案(未知)。
室友為了幫助他寫作業提供了一個方法,該方法有兩個入參,一個是作業題目,一個是該學生的引用(解出答案得知道往哪寫)。
程式執行時,該學生只要呼叫室友的代寫作業方法就行了。一旦室友得到答案,因為有該學生的引用,所以直接找到對應方法,幫助其完成作業。
再複雜點描述:
學生呼叫室友的替寫作業方法,註冊了題目和自己的引用。室友的替寫作業方法被呼叫,則會根據題目完成作業後,再回調該同學寫作業方法,完成作業。
再抽象點描述:
類A呼叫類B的方法b(傳入相關資訊),類B的方法在執行完後,會將結果寫到(再回調)類A的方法a,完成動作。(其實方法a就是傳說中的回撥方法啦)
最抽象的描述:
呼叫,回撥。
介面方式的回撥方法
常常使用回撥方法的同學可能會說,我從來也沒見過直接把物件的引用寫到第一次呼叫方法裡面的。
嗯,是的,下面就來填上述例子留下的“天坑”(實際專案中常見到)。
問題:在呼叫方法中直接傳物件引用進去有什麼不好?
只說一點,只是讓別人代寫個方法,犯得著把自己全部暴露給別人嗎。萬一這個別人是競爭對手的介面咋辦。這就是傳說中的後面程式碼嗎(/tx)。
總之這樣做是非常不安全的。
因此,最常規的《呼叫,回撥》實現,是(你已經猜到了)使用介面作為引用(說的不嚴謹)傳入呼叫的方法裡面。
我承認,怎麼將思路跳轉到使用介面的花了我好長時間。
我們再看RoomMate類的getAnswer方法。
public class RoomMate {
public void getAnswer(String homework, Student student) {
if("1+1=?".equals(homework)) {
student.doHomeWork(homework, "2");
} else {
student.doHomeWork(homework, "(空白)");
}
}
}
關鍵在於,該方法的用途是來解決某學生提出的某個問題。答案是通過學生的doHomeWork方法回撥傳回去的。那假設有個工人也有問題,這位室友該怎麼解決呢。再開個方法,專門接收工人的引用作為傳參?當然不用,只要你這個引用包含了doHomeWork()方法,那麼不論你是工人、警察還是環衛工人,直接呼叫getAnswer()方法就能解決你提的問題。
至此我們的思路達到了:所有的物件要有同一個方法,所以自熱而然就引出了介面概念。只要這些物件都實現了某個介面就行了,這個介面的作用,僅僅是用來規定那個做作業的方法長什麼樣。這樣工人實現了該介面,那麼就有了預設繼承的做作業方法。工人再把自己的引用拋給該室友的時候,這個室友就不需要改動任何程式碼,直接接觸答案,完成任務了。
建立一個做作業的介面,專門規定,需要哪些東西(問題和答案)就能做作業.
public interface DoHomeWork {
void doHomeWork(String question, String answer);
}
改動下中國好室友的解答方法。任意一個實現了DoHomeWork 介面的someone,都擁有doHomeWork(String question,String answer)的方法。這個方法就是上面已經提到的“回撥方法”。someone先呼叫下好室友的getAnswer()方法,把問題和自己傳進來(此為呼叫),好室友把問題解答出之後,呼叫預設提供的方法,寫完作業。
思考下,因為是以介面作為引數型別的約定,在普通物件upcast向上轉型之後將只暴露介面描述的那個方法,別人獲取到這個引用,也只能使用這個(回撥)方法。至此,遺留的重大安全隱患重要解決。
完整程式碼
public class RoomMate {
public void getAnswer(String homework, DoHomeWork someone) {
if("1+1=?".equals(homework)) {
someone.doHomeWork(homework, "2");
} else {
someone.doHomeWork(homework, "(空白)");
}
}
}
package org.futeng.designpattern.callback.test1;
public class Worker implements DoHomeWork {
@Override
public void doHomeWork(String question, String answer) {
System.out.println("作業本");
if(answer != null) {
System.out.println("作業:"+question+" 答案:"+ answer);
} else {
System.out.println("作業:"+question+" 答案:"+ "(空白)");
}
}
public static void main(String[] args) {
Worker worker = new Worker();
String question = "1+1=?";
new RoomMate().getAnswer(question, worker);
}
}
執行程式
作業本
作業:1+1=? 答案:2
至此,呼叫+回撥的文章是不是寫完了呢。
咳咳,還木有。大家喝點茶再忍耐下。(我都寫一天了 - -)
常規使用之匿名內部類
作為平凡的屁民,實用主義是我們堅持的生存法則。
所以凡事用不到的技術都可以不學,凡事學了卻不用的技術等於白學。
我們之前已經定性,中國好室友RoomMate類擁有接受任何人任何問題挑戰的潛質。
自從好室友出名之後,有個不知道什麼工作(型別)的人也來問問題。反正只要實現了回撥介面,好室友都能呼叫你預設繼承的回撥方法,那就放馬過來吧。
package org.futeng.designpattern.callback.test1;
public class RoomMate {
public void getAnswer(String homework, DoHomeWork someone) {
if("1+1=?".equals(homework)) {
someone.doHomeWork(homework, "2");
} else {
someone.doHomeWork(homework, "(空白)");
}
}
public static void main(String[] args) {
RoomMate roomMate = new RoomMate();
roomMate.getAnswer("1+1=?", new DoHomeWork() {
@Override
public void doHomeWork(String question, String answer) {
System.out.println("問題:"+question+" 答案:"+answer);
}
});
}
}
看到稍顯奇怪的roomMate.getAnswer("1+1=?", new DoHomeWork() {的哪一行,其實這裡new的是DoHomeWork介面的一個匿名內部類。這裡我想大家應該自己動腦想想,呼叫+反調,這個過程是怎麼實現的了。
至於是否使用匿名內部類是根據具體使用場景決定的。普通類不夠直接,匿名內部類的語法似乎也不夠友好。
上述匿名內部類的示例才是開源工具中常見到的使用方式。
呼叫roomMate解答問題的方法(傳進去自己的引用),roomMate解決問題,回撥引用裡面包含的回撥方法,完成動作。
roomMate就是個工具類,“呼叫”這個方法你傳進去兩個引數(更多也是一個道理),什麼問題,問題解決了放哪,就行了。該“呼叫”方法根據問題找到答案,就將結果寫到你指定放置的位置(作為回撥方法的入參)。
試想下,“中國好室友”接收的問題是SQL語句,接收的放置位置是我的引用。你解決問題(執行完SQL),將答案(SQL的反饋結果)直接寫入我的回撥方法裡面。回撥方法裡面可能包括一個個的欄位賦值。但是在呼叫層面隱藏了很多細節的處理。這是回撥方法的一個小小的優勢。再換句話說,不需要拿到執行完SQL之後的返回結果一個個來賦值。
SQL的例子
public static List<Person> queryPerson() {
QueryRunner queryRunner = new QueryRunner(DataSourceSupport.getDataSource());
return queryRunner.query(" select t.name, t.age from person t ", new ResultSetHandler<List<Person>>(){
List list = new ArrayList<Person>();
@Override
public List<Person> handle(ResultSet rs) throws SQLException {
while(rs.next()) {
Person person = new Person();
person.setName(rs.getString(0));
person.setAge(rs.getInt(1));
list.add(person);
}
return list;
}
});
}
回撥方法的優勢
回撥方法最大的優勢在於,非同步回撥,這樣是其最被廣為使用的原因。
下面將沿用“中國好室友” 來對回撥方法做非同步實現。
回撥介面不用變
public interface DoHomeWork {
void doHomeWork(String question, String answer);
}
為了體現非同步的意思,我們給好室友設定了個較難的問題,希望好室友能多好點時間思考。
Student student = new Student();
String homework = "當x趨向於0,sin(x)/x =?";
#給學生新建個ask方法,該方法中另開一個執行緒,來等待回撥方法的結果反饋。
student.ask(homework, new RoomMate());
#ask方法如下
public void ask(final String homework, final RoomMate roomMate) {
new Thread(new Runnable() {
@Override
public void run() {
roomMate.getAnswer(homework, Student.this);
}
}).start();
goHome();
}
#新開的執行緒純粹用來等待好室友來寫完作用。由於在好室友類中設定了3秒的等待時間,所以可以看到goHome方法將先執行。
#意味著該學生在告知好室友做作用後,就可以做自己的事情去了,不需要同步阻塞去等待結果。
#一旦好室友完成作用,寫入作業本,該現場也就結束運行了。
完整程式碼
public class Student implements DoHomeWork{
@Override
public void doHomeWork(String question, String answer) {
System.out.println("作業本");
if(answer != null) {
System.out.println("作業:"+question+" 答案:"+ answer);
} else {
System.out.println("作業:"+question+" 答案:"+ "(空白)");
}
}
public void ask(final String homework, final RoomMate roomMate) {
new Thread(new Runnable() {
@Override
public void run() {
roomMate.getAnswer(homework, Student.this);
}
}).start();
goHome();
}
public void goHome(){
System.out.println("我回家了……好室友,幫我寫下作業。");
}
public static void main(String[] args) {
Student student = new Student();
String homework = "當x趨向於0,sin(x)/x =?";
student.ask(homework, new RoomMate());
}
}
public class RoomMate {
public void getAnswer(String homework, DoHomeWork someone) {
if ("1+1=?".equals(homework)) {
someone.doHomeWork(homework, "2");
} else if("當x趨向於0,sin(x)/x =?".equals(homework)) {
System.out.print("思考:");
for(int i=1; i<=3; i++) {
System.out.print(i+"秒 ");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println();
someone.doHomeWork(homework, "1");
} else {
someone.doHomeWork(homework, "(空白)");
}
}
}
完結至此回撥方法的介紹告一段落。
趁著是高考日,表示要跟考生感同身受一下。特意花了整整一個白天的時間寫就這篇文章。全文一蹴而就並沒有更多時間來修改,可能隱藏著諸多錯誤,行文也可能不甚通順,還請各位指正和海涵。