前文从策略模式说起,一步一步引出了 lambda
表达式,同时也提到了函数式接口。那么,什么是函数式接口?
什么是函数式接口
函数式接口,就是只有一个抽象方法的接口,可用于 lambda 表达式的接口。
先从 jdk 源码来看,找个大家基本都用过的:Runnable
。
1 |
|
还是从使用上开始举例说明。比如,现在有一个任务要放到线程池里。
1 | public void oldWay() { |
此处,线程池真正需要的,是告诉线程池要做什么事。也就是说,这个环境下,它真正期望得到的,是一段有处理逻辑的函数,并且这个函数主体的类型,与要求的参数类型一致。
而 Runnable
是一个函数式接口,上面的写法就可以简化成:
1 | public void functionalWay() { |
类型推导
在前文讲 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 |
|
然后,我们定义含有重载方法的类。
1 | public class ExplicitTypeExample { |
如果我们像下面这样去省略类型,则编译时会报错。
1 | public static void main(String[] args) { |
此时,如果推导的话,会发现,InterfaceA
和 InterfaceB
两个接口都满足要求,所以编译时会报错。
java: 对sayHello的引用不明确
ExplicitTypeExample 中的方法 sayHello(InterfaceA) 和 ExplicitTypeExample 中的方法 sayHello(InterfaceB) 都匹配
所以,当类型无法自动推导出来时,需要显式指定。
1 | public static void main(String[] args) { |
实际使用
纸上得来终觉浅,学了就要实践它。
背景:你做了个电商系统,需要支持用户通过不同的渠道来付款。
场景一:用户选择某付款方式,后端返回前端该付款方式需要的信息。
场景二:用户付款之后,有支付回调,在回调里要验证来源的有效性。
场景一处理
首先,我们定义一个函数式接口 PayMethodInfoService
,带有抽象方法 getPayMethodInfo()
,不管是哪种支付方式,均实现该接口。
1 |
|
然后处理我们的业务逻辑:
1 | public class PayController { |
场景二处理
同样的,我们先定义函数式接口 PayCallbackCheckService
,带有抽象方法 verify
。当微信支付或者支付宝支付的回调校验时,都使用该接口。
1 |
|
业务逻辑处理:
1 | public class CallbackController { |
总结
函数式接口,其实就是只有一个抽象方法的接口,可用于 lambda 表达式。而 lambda 表达式返回的,其实是函数式接口的其中一个实现方式的实例化对象。
比如,
1 | Runnable r = new Runnable() { |
和下面的方式是一样的:
1 | Runnable r = () -> System.out.println("im am r1"); |
() -> System.out.println("im am r1")
是一个 lambda 表达式,也是 Runnable
的一个实例化对象。
函数式接口出现的地方,实际期望得到的是一个符合要求的函数。 lambda 表达式不能脱离上下文而存在,它必须要有一个明确的目标类型,而这个目标类型就是某个函数式接口。不要在意类名,不要在意方法名,你要的,只是一个处理过程。
通过上面的实际使用,我们发现,两个场景下我们定义了两个函数式接口。而这两个函数式接口,名字叫什么不重要,方法名也不重要,为什么还需要再反复自定义函数式接口?
我们能想到的,JDK
的开发人员也已经想到了。所以,在 JDK
里,已经提供了有多个函数式接口,基本可以满足我们不同场景的需求。
注:本文配套代码可在
github
查看:stream-and-lambda