写在前面
前面说过,判断一门语言是否支持函数式编程,一个重要的判断标准就是:它是否将函数看做是“第一等公民(first-class citizens)”。
函数是“第一等公民”,意味着函数和其它数据类型具备同等的地位——可以赋值给某个变量,可以作为另一个函数的参数,也可以作为另一个函数的返回值。
Java 8是通过函数式接口,赋予了函数“第一等公民”的特性。
本文将详细介绍Java 8中的函数式接口。
本文的示例代码可从gitee上获取,gitee.com/cnmemset/ja…
完整专栏文章获取,可关注公众号【员说】。
函数式接口
什么是函数式接口(function interface)?只有一个抽象方法的接口都属于函数式接口。
按照规范,我们强烈建议在定义函数式接口时,加上注解 @FunctionalInterface,这样在编译阶段就可以判断该接口是否符合函数式接口的规范。当然,也可以不加注解 @FunctionalInterface,这并不影响函数式接口的定义和使用。
以下是一个典型的函数式接口 Consumer:
1 | java复制代码// 强烈建议加上注解 @FunctionalInterface |
函数式接口本质是一个接口(interface),所以我们可以通过一个具体的类(包括匿名类)来实现一个函数式接口。但与普通接口不同,函数式接口的实现还可以是一个lambda表达式,甚至可以是一个方法引用(method reference)。
下面,我们逐一介绍JDK中内置的一些典型的函数式接口。
Java 8中内置的函数式接口
Java 8新增的内置函数式接口都在包 java.util.function 中定义,主要包括:
- Functions
Function
在代码世界,最为常见的一种函数式接口是接收一个参数值,然后返回一个响应值。JDK提供了一个标准的泛型函数式接口 Function:
1 | java复制代码@FunctionalInterface |
Function的一个经典应用场景是Map的computeIfAbsent函数。
1 | java复制代码public V computeIfAbsent(K key, |
computeIfAbsent函数会先判断对应key在map中是否存在,如果key不存在,则通过参数 mappingFunction 来计算得出一个value,并将这个键值对写入到map中,并返回计算出来的value。如果key已存在,则返回map中key对应的value。
假设一个应用场景,我们要构建一个HashMap,key是某个单词,value是单词的字母长度。实例代码如下:
1 | java复制代码public static void testFunctionWithLambda() { |
上面的实例会输出:
1 | ini复制代码5 |
注意到代码片段“s -> s.length()”,这是一个典型的lambda表达式,含义等同于函数:
1 | java复制代码public static int getStringLength(String s) { |
更详尽具体的lambda表达式的介绍可以参考随后的系列文章。
之前提到过,函数式接口也可以通过一个方法引用(method reference)来实现。实例代码如下:
1 | java复制代码public static void testFunctionWithMethodReference() { |
注意到方法引用“String::length”,Java 8允许我们将一个实例方法转化成一个函数式接口的实现。 它的含义和 lambda 表达式 “s -> s.length()” 是相同的。
更详尽具体的方法引用的介绍可以参考随后的系列文章。
BiFunction
Function 限制了只能有一个参数,但两个参数的情形也非常常见,所以就有了BiFunction,它接收两个参数值,然后返回一个响应值。
1 | java复制代码@FunctionalInterface |
Function的一个经典应用场景是Map的replaceAll函数。
1 | java复制代码public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) |
Map的replaceAll函数,会遍历Map中的所有Entry,通过BiFunction类型的参数 function 计算出一个新值,然后用新值替换旧值。
假设一个应用场景,我们使用一个HashMap,记录了一些单词和它们的长度,接着产品经理提了一个新需求,要求对某些指定的单词,长度统一记录为0。实例代码如下:
1 | java复制代码public static void testBiFunctionWithLambda() { |
上述代码的输出为:
1 | ini复制代码{world=5, at=0, hello=5, on=0} |
- Supplier
除了Function和BiFunction,还有一种常见的函数式接口是不需要任何参数,直接返回一个响应值。这就是Supplier:
1 | java复制代码@FunctionalInterface |
Supplier的一个典型应用场景是快速实现了工厂类的生产方法,包括延时的或者异步的生产方法。实例代码如下:
1 | java复制代码public class SupplierExample { |
上述代码输出类似:
1 | erlang复制代码26 |
- Consumers
如果说Supplier属于生产者,那与之相对的是消费者Consumer。
Consumer
与Supplier相反,Consumer 接收一个参数,而不返回任何值。
1 | java复制代码@FunctionalInterface |
示例代码:
1 | java复制代码public static void testConsumer() { |
上述代码的输出为:
1 | 复制代码Guangdong |
BiConsumer
还有BiConsumer,语义和Consumer一致,不同的是BiConsumer接收2个参数。
1 | java复制代码@FunctionalInterface |
示例代码:
1 | java复制代码public static void testBiConsumer() { |
上述代码的输出是:
1 | 复制代码Guangdong 的省会是 Guangzhou |
- Predicate
Predicate 的含义是接收一个参数值,然后依据给定的断言条件,返回一个boolean值。它实质上一个特殊的 Function,一个指定了返回值类型为boolean的 Function。
1 | java复制代码@FunctionalInterface |
Predicate 的使用场景通常是用来作为某种过滤条件。实例代码:
1 | java复制代码public static void testPredicate() { |
上述代码是过滤掉以字母 G 开头的省份,输出为:
1 | csharp复制代码true |
- Operators
Operator 函数式接口是一种特殊的 Function,要求返回值类型和参数类型是相同的。
和 Function/BiFunction 一样,Operators 也支持1个或2个参数。
UnaryOperator
UnaryOperator 支持1个参数,UnaryOperator
1 | java复制代码@FunctionalInterface |
UnaryOperator的示例代码——将省份拼音转换大写与小写字母:
1 | java复制代码public static void testUnaryOperator() { |
上述代码输出为:
1 | csharp复制代码[GUANGDONG, JIANGSU, GUANGXI, JIANGXI, SHANDONG] |
BinaryOperator
BinaryOperator 支持2个参数,BinaryOperator
1 | java复制代码@FunctionalInterface |
BinaryOperator的示例代码 —— 计算List中的所有整数的和:
1 | java复制代码public static void testBinaryOperator() { |
上述代码的输出为:
1 | 复制代码27 |
- Java 7及之前版本遗留的函数式接口
前面提到过函数式接口的定义:只有一个抽象方法的接口都属于函数式接口。
按照这个定义,在Java 7或之前版本中定义的一些“老”接口也属于函数式接口,包括:
Runnable、Callable、Comparator等等。
当然,这些遗留的函数式接口,在Java 8中也加上了注解 @FunctionalInterface 。
组合函数式接口
我们在第一篇提到过:函数式编程是一种编程范式(programming paradigm),追求的目标是整个程序都由函数调用以及函数组合构成的。
函数组合(function composing),指的是将一系列简单函数组合起来形成一个复合函数。
Java 8中的函数式接口也提供了函数组合的功能。大家注意观察,可以发现基本每个内置的函数式接口都有一个非抽象的方法 andThen。andThen方法的功能是将多个函数式接口组合在一起,以串行的顺序逐一执行,从而形成一个新的函数式接口。
以Consumer.andThen方法为例,它返回一个新的Consumer实例。新的Consumer实例会先执行当前的accpet方法,然后再执行 after 的accpet方法。源码片段如下:
1 | java复制代码@FunctionalInterface |
示例代码如下:
1 | java复制代码public static void testConsumerAndThen() { |
上述代码的输出是:
1 | 复制代码GUANGDONG |
Function.andThen 方法则更复杂一些,它返回一个新的Function实例,在新的Function中,会先用类型为 T 的参数 t 执行当前的apply方法,得到一个类型为 R 的返回值 r,然后将 r 作为输入参数,继续执行 after 的apply方法,最终得到一个类型为 V 的返回值:
1 | java复制代码@FunctionalInterface |
代码示例:
1 | java复制代码public static void testFunctionAndThen() { |
上述代码输出为:
1 | ini复制代码{at=false, world=true, hello=true, on=false} |
结语
Java 8是通过函数式接口,赋予了函数“第一等公民”的特性。
通过函数式接口,使得函数和其它数据类型一样,可以赋值给某个变量、可以作为另一个函数的参数、也可以作为另一个函数的返回值。
函数式接口的实现,可以是一个类(包括匿名类),但更多的是一个lambda表达式或者一个方法引用(method reference)。
本文转载自: 掘金