0%

stream and lambda(2) - 函数式接口简介

前文从策略模式说起,一步一步引出了 lambda 表达式,同时也提到了函数式接口。那么,什么是函数式接口?

什么是函数式接口

函数式接口,就是只有一个抽象方法的接口,可用于 lambda 表达式的接口。

先从 jdk 源码来看,找个大家基本都用过的:Runnable

1
2
3
4
@FunctionalInterface
public interface Runnable {
public abstract void run();
}

还是从使用上开始举例说明。比如,现在有一个任务要放到线程池里。

1
2
3
4
5
6
7
8
9
public void oldWay() {
ExecutorService es = Executors.newSingleThreadExecutor();
es.submit(new Runnable() {
@Override
public void run() {
System.out.println("十万火急,借过借过");
}
});
}

此处,线程池真正需要的,是告诉线程池要做什么事。也就是说,这个环境下,它真正期望得到的,是一段有处理逻辑的函数,并且这个函数主体的类型,与要求的参数类型一致。

Runnable 是一个函数式接口,上面的写法就可以简化成:

1
2
3
4
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 已经有了这个东西,可能你只把它当成了语法糖。

1
Set<String> s = new HashSet<>();

Java7中已经可以省略构造函数的泛型类型,只不过 Java 8更进一步,可以把 lambda 表达式里的所有参数类型都省略,不用再显式声明类型。

其实,在这里也算回答上面的一个隐藏的问题:为什么函数式接口,只能有一个抽象方法?因为有多个抽象方法,推导不出来到底用的是哪个。

所以,如果自己有需要定义函数式接口的时候,注意不要定义多个抽象接口。当然,为了避免自己有时候不小心,可以在接口上加上 @FunctionalInterface,这样编译器就会自动帮你检查了。

所有类型都可以推导出来吗

在类型推导的时候,其实是结合 lambda 表达式的上下文来推导的。比如上面的推导过程,其实是考虑了目标对象的实际情况,包括方法名,参数,返回结果。当通过这些都无法唯一确定的时候,就必须要显示指定类型了,比如下面的例子。

首先,我们定义两个函数式接口。

1
2
3
4
5
6
7
8
9
@FunctionalInterface
interface InterfaceA {
String name();
}

@FunctionalInterface
interface InterfaceB {
String name();
}

然后,我们定义含有重载方法的类。

1
2
3
4
5
6
7
8
9
10
11
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());
}

}

如果我们像下面这样去省略类型,则编译时会报错。

1
2
3
4
public static void main(String[] args) {
ExplicitTypeExample example = new ExplicitTypeExample();
example.sayHello(() -> "interfaceA");
}

此时,如果推导的话,会发现,InterfaceAInterfaceB 两个接口都满足要求,所以编译时会报错。

java: 对sayHello的引用不明确

ExplicitTypeExample 中的方法 sayHello(InterfaceA) 和 ExplicitTypeExample 中的方法 sayHello(InterfaceB) 都匹配

所以,当类型无法自动推导出来时,需要显式指定。

1
2
3
4
public static void main(String[] args) {
ExplicitTypeExample example = new ExplicitTypeExample();
example.sayHello((InterfaceA)() -> "interfaceA");
}

实际使用

纸上得来终觉浅,学了就要实践它。

背景:你做了个电商系统,需要支持用户通过不同的渠道来付款。

场景一:用户选择某付款方式,后端返回前端该付款方式需要的信息。

场景二:用户付款之后,有支付回调,在回调里要验证来源的有效性。

场景一处理

首先,我们定义一个函数式接口 PayMethodInfoService ,带有抽象方法 getPayMethodInfo(),不管是哪种支付方式,均实现该接口。

1
2
3
4
@FunctionalInterface
public interface PayMethodInfoService {
PayMethodInfo getPayMethodInfo();
}

然后处理我们的业务逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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。当微信支付或者支付宝支付的回调校验时,都使用该接口。

1
2
3
4
@FunctionalInterface
public interface PayCallbackCheckService {
boolean verify(PayCallbackInfo info);
}

业务逻辑处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 表达式返回的,其实是函数式接口的其中一个实现方式的实例化对象。

比如,

1
2
3
4
5
6
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("im am r1");
}
};

和下面的方式是一样的:

1
Runnable r = () -> System.out.println("im am r1");

() -> System.out.println("im am r1") 是一个 lambda 表达式,也是 Runnable 的一个实例化对象。

函数式接口出现的地方,实际期望得到的是一个符合要求的函数。 lambda 表达式不能脱离上下文而存在,它必须要有一个明确的目标类型,而这个目标类型就是某个函数式接口。不要在意类名,不要在意方法名,你要的,只是一个处理过程。

通过上面的实际使用,我们发现,两个场景下我们定义了两个函数式接口。而这两个函数式接口,名字叫什么不重要,方法名也不重要,为什么还需要再反复自定义函数式接口?

我们能想到的,JDK 的开发人员也已经想到了。所以,在 JDK里,已经提供了有多个函数式接口,基本可以满足我们不同场景的需求。

注:本文配套代码可在 github 查看:stream-and-lambda