stream and lambda(2) - 函式式介面簡介

語言: CN / TW / HK

前文從策略模式說起,一步一步引出了 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");
}

此時,如果推導的話,會發現, InterfaceAInterfaceB 兩個介面都滿足要求,所以編譯時會報錯。

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