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