stream and lambda(2) - 函式式介面簡介
前文從策略模式說起,一步一步引出了 lambda
表示式,同時也提到了函式式介面。那麼,什麼是函式式介面?
什麼是函式式介面
函式式介面,就是隻有一個抽象方法的介面,可用於 lambda 表示式的介面。
先從 jdk 原始碼來看,找個大家基本都用過的: Runnable
。
@FunctionalInterface public interface Runnable { public abstract void run(); }
還是從使用上開始舉例說明。比如,現在有一個任務要放到執行緒池裡。
public void oldWay() { ExecutorService es = Executors.newSingleThreadExecutor(); es.submit(new Runnable() { @Override public void run() { System.out.println("十萬火急,借過借過"); } }); }
此處,執行緒池真正需要的,是告訴執行緒池要做什麼事。也就是說,這個環境下,它真正期望得到的,是一段有處理邏輯的函式,並且這個函式主體的型別,與要求的引數型別一致。
而 Runnable
是一個函式式介面,上面的寫法就可以簡化成:
public void functionalWay() { ExecutorService es = Executors.newSingleThreadExecutor(); es.submit(() -> System.out.println("I am functional way")); }
型別推導
在前文講 lambda 表示式的時候就提到,寫法上能省則省,那麼為什麼型別可以省掉,邏輯上又是怎麼推匯出來型別的呢?
先看 es.submit
,一共有三個:
1.< T > Future< T > submit(Callable< T > task); 2.< T > Future< T > submit(Runnable task, T result); 3.Future<?> submit(Runnable task);
人為推導可以這麼來:
1.引數只有一個,排除2號嫌疑人 2.函式式介面無參,1號3號都有嫌疑 3.函式式介面無返回值,真相只有一個,就是3號
上面是人為推導,交給 Java 這個偵探來做,思路其實也一樣。
是不是感覺很神奇,Java8 突然就多了一項神通?其實不然,在 Java7 已經有了這個東西,可能你只把它當成了語法糖。
Set<String> s = new HashSet<>();
Java7中已經可以省略建構函式的泛型型別,只不過 Java 8更進一步,可以把 lambda 表示式裡的所有引數型別都省略,不用再顯式宣告型別。
其實,在這裡也算回答上面的一個隱藏的問題:為什麼函式式介面,只能有一個抽象方法?因為有多個抽象方法,推導不出來到底用的是哪個。
所以,如果自己有需要定義函式式介面的時候,注意不要定義多個抽象介面。當然,為了避免自己有時候不小心,可以在介面上加上 @FunctionalInterface
,這樣編譯器就會自動幫你檢查了。
所有型別都可以推匯出來嗎
在型別推導的時候,其實是結合 lambda 表示式的上下文來推導的。比如上面的推導過程,其實是考慮了目標物件的實際情況,包括方法名,引數,返回結果。當通過這些都無法唯一確定的時候,就必須要顯示指定型別了,比如下面的例子。
首先,我們定義兩個函式式介面。
@FunctionalInterface interface InterfaceA { String name(); } @FunctionalInterface interface InterfaceB { String name(); }
然後,我們定義含有過載方法的類。
public class ExplicitTypeExample { public void sayHello(InterfaceA interfaceA) { System.out.print("hello, im am " + interfaceA.name()); } public void sayHello(InterfaceB interfaceB) { System.out.print("hello, im am " + interfaceB.name()); } }
如果我們像下面這樣去省略型別,則編譯時會報錯。
public static void main(String[] args) { ExplicitTypeExample example = new ExplicitTypeExample(); example.sayHello(() -> "interfaceA"); }
此時,如果推導的話,會發現, InterfaceA
和 InterfaceB
兩個介面都滿足要求,所以編譯時會報錯。
java: 對sayHello的引用不明確 ExplicitTypeExample 中的方法 sayHello(InterfaceA) 和 ExplicitTypeExample 中的方法 sayHello(InterfaceB) 都匹配
所以,當型別無法自動推匯出來時,需要顯式指定。
public static void main(String[] args) { ExplicitTypeExample example = new ExplicitTypeExample(); example.sayHello((InterfaceA)() -> "interfaceA"); }
實際使用
紙上得來終覺淺,學了就要實踐它。
背景:你做了個電商系統,需要支援使用者通過不同的渠道來付款。
場景一:使用者選擇某付款方式,後端返回前端該付款方式需要的資訊。
場景二:使用者付款之後,有支付回撥,在回撥裡要驗證來源的有效性。
場景一處理
首先,我們定義一個函式式介面 PayMethodInfoService
,帶有抽象方法 getPayMethodInfo()
,不管是哪種支付方式,均實現該介面。
@FunctionalInterface public interface PayMethodInfoService { PayMethodInfo getPayMethodInfo(); }
然後處理我們的業務邏輯:
public class PayController { public PayMethodInfo getPreparePayInfo(int payMethod) throws Exception { PayMethodInfo info; switch (payMethod) { case 1 : info = getPreparePayInfo(() -> new PayMethodInfo(1, "支付寶支付", "12345")); break; case 2: info = getPreparePayInfo(() -> new PayMethodInfo(2, "微信支付", "12345")); break; default: throw new Exception("傳入支付方式不正確"); } return info; } private PayMethodInfo getPreparePayInfo(PayMethodInfoService payMethodInfoService) { return payMethodInfoService.getPayMethodInfo(); } }
場景二處理
同樣的,我們先定義函式式介面 PayCallbackCheckService
,帶有抽象方法 verify
。當微信支付或者支付寶支付的回撥校驗時,都使用該介面。
@FunctionalInterface public interface PayCallbackCheckService { boolean verify(PayCallbackInfo info); }
業務邏輯處理:
public class CallbackController { public void alipayCallback(PayCallbackInfo info) throws Exception { checkValid(info, info1 -> { System.out.println("this is alipay callback, it's invalid"); return false; }); //其他的一些邏輯處理 } public void wechatCallback(PayCallbackInfo info) throws Exception { checkValid(info, info1 -> { System.out.println("this is wechat callback, it's valid"); return true; }); //其他的一些邏輯處理 } private void checkValid(PayCallbackInfo info, PayCallbackCheckService service) throws Exception { if (!service.verify(info)) { throw new Exception("資訊校驗失敗,非法回撥"); } } }
總結
函式式介面,其實就是隻有一個抽象方法的介面,可用於 lambda 表示式。而 lambda 表示式返回的,其實是函式式介面的其中一個實現方式的例項化物件。
比如,
Runnable r = new Runnable() { @Override public void run() { System.out.println("im am r1"); } };
和下面的方式是一樣的:
Runnable r = () -> System.out.println("im am r1");
() -> System.out.println("im am r1")
是一個 lambda 表示式,也是 Runnable
的一個例項化物件。
函式式接口出現的地方, 實際期望得到的是一個符合要求的函式 。 lambda 表示式不能脫離上下文而存在,它必須要有一個明確的目標型別,而這個目標型別就是某個函式式介面。不要在意類名,不要在意方法名,你要的,只是一個處理過程。
通過上面的實際使用,我們發現,兩個場景下我們定義了兩個函式式介面。而這兩個函式式介面,名字叫什麼不重要,方法名也不重要,為什麼還需要再反覆自定義函式式介面?
我們能想到的, JDK
的開發人員也已經想到了。所以,在 JDK
裡,已經提供了有多個函式式介面,基本可以滿足我們不同場景的需求。
注:本文配套程式碼可在 github
檢視: stream-and-lambda
- stream and lambda(18) - 終止操作之 stream 收集器 collectors
- stream and lambda(15) - 終止操作之 stream 陣列操作 toArray
- stream and lambda(13) - 終止操作之計數 count 與比較 min、max
- stream and lambda(12) - 終止操作之查詢與匹配(findAny、findFirst、allMatch、anyMatch、noneMatch)
- stream and lambda(11) - Optional 簡介
- stream and lambda(10) - 中間操作之排序sorted與除錯peek
- stream and lambda(9) - 中間操作之map操作(map、flatmap)
- stream and lambda(8) - 中間操作之篩選操作(filter、distinct、limit、skip)
- stream and lambda(7) - stream 的建立
- stream and lambda(6) - stream 簡介
- stream and lambda(5) - lambda 表示式最佳實踐
- stream and lambda(3) - jdk 提供的函式式介面
- stream and lambda(2) - 函式式介面簡介
- stream and lambda(1) - lambda 簡介