3.4 使用函数式接口
就像你在3.2.1节中学到的,函数式接口定义且只定义了一个抽象方法。函数式接口很有用,因为抽象方法的签名可以描述Lambda表达式的签名。函数式接口的抽象方法的签名称为函数描述符。所以为了应用不同的Lambda表达式,你需要一套能够描述常见函数描述符的函数式接口。Java API中已经有了几个函数式接口,比如你在3.2节中见到的Comparator、Runnable和Callable。
Java 8的库设计师帮你在java.util.function包中引入了几个新的函数式接口。我们接下来会介绍Predicate、Consumer和Function,更完整的列表可见本节结尾处的表3-2。
3.4.1 Predicate
java.util.function.Predicate接口定义了一个名叫test的抽象方法,它接受泛型T对象,并返回一个boolean。这恰恰和你先前创建的一样,现在就可以直接使用了。在你需要表示一个涉及类型T的布尔表达式时,就可以使用这个接口。比如,你可以定义一个接受String对象的Lambda表达式,如下所示。
代码清单 3-2 使用
Predicate
@FunctionalInterfacepublic interface Predicate<T> {boolean test(T t);}public <T> List<T> filter(List<T> list, Predicate<T> p) {List<T> results = new ArrayList<>();for(T t: list) {if(p.test(t)) {results.add(t);}}return results;}Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();List<String> nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);
如果你去查Predicate接口的Javadoc说明,可能会注意到诸如and和or等其他方法。现在你不用太计较这些,3.8节会讨论。
3.4.2 Consumer
java.util.function.Consumer接口定义了一个名叫accept的抽象方法,它接受泛型T的对象,没有返回(void)。你如果需要访问类型T的对象,并对其执行某些操作,就可以使用这个接口。比如,你可以用它来创建一个forEach方法,接受一个Integers的列表,并对其中每个元素执行操作。在下面的代码中,你就可以使用这个forEach方法,并配合Lambda来打印列表中的所有元素。
代码清单 3-3 使用
Consumer
@FunctionalInterfacepublic interface Consumer<T>{void accept(T t);}public <T> void forEach(List<T> list, Consumer<T> c){for(T i: list){c.accept(i);}}forEach(Arrays.asList(1,2,3,4,5),(Integer i) -> System.out.println(i) ←----Lambda是Consumer中accept方法的实现);
3.4.3 Function
java.util.function.Function接口定义了一个叫作apply的抽象方法,它接受泛型T的对象,并返回一个泛型R的对象。如果你需要定义一个Lambda,将输入对象的信息映射到输出,就可以使用这个接口(比如提取苹果的重量,或把字符串映射为它的长度)。在下面的代码中,我们向你展示如何利用它来创建一个map方法,以将一个String列表映射到包含每个String长度的Integer列表。
代码清单 3-4 使用
Function
@FunctionalInterfacepublic interface Function<T, R> {R apply(T t);}public <T, R> List<R> map(List<T> list, Function<T, R> f) {List<R> result = new ArrayList<>();for(T t: list) {result.add(f.apply(t));}return result;}// [7, 2, 6]List<Integer> l = map(Arrays.asList("lambdas", "in", "action"),(String s) -> s.length() ←----Lambda是Function接口的apply方法的实现);
基本类型特化
我们介绍了三个泛型函数式接口:Predicate、Consumer和Function。还有些函数式接口专为某些类型而设计。
回顾一下:Java类型要么是引用类型(比如Byte、Integer、Object、List),要么是基本类型(比如int、double、byte、char)。但是泛型(比如Consumer中的T)只能绑定到引用类型。这是由泛型内部的实现方式造成的。2 因此,在Java里有一个将基本类型转换为对应的引用类型的机制。这个机制叫作装箱(boxing)。相反的操作,也就是将引用类型转换为对应的基本类型,叫作拆箱(unboxing)。Java还有一个自动装箱机制来帮助程序员执行这一任务:装箱和拆箱操作是自动完成的。比如,这就是为什么下面的代码是有效的(一个int被装箱成为Integer):
2C#等其他语言没有这一限制。Scala等语言只有引用类型。第20章会再次探讨这个问题。
List<Integer> list = new ArrayList<>();for (int i = 300; i < 400; i++){list.add(i);}
但这在性能方面是要付出代价的。装箱后的值本质上就是把基本类型包裹起来,并保存在堆里。因此,装箱后的值需要更多的内存,并需要额外的内存搜索来获取被包裹的基本值。
Java 8为前面所说的函数式接口带来了一个专门的版本,以便在输入和输出都是基本类型时避免自动装箱的操作。比如,在下面的代码中,使用IntPredicate就避免了对值1000进行装箱操作,但要是用Predicate就会把参数1000装箱到一个Integer对象中:
public interface IntPredicate {boolean test(int t);}IntPredicate evenNumbers = (int i) -> i % 2 == 0;evenNumbers.test(1000); ←---- true(无装箱)Predicate<Integer> oddNumbers = (Integer i) -> i % 2 != 0;oddNumbers.test(1000); ←---- false(装箱)
一般来说,针对专门的输入参数类型的函数式接口的名称都要加上对应的基本类型前缀,比如DoublePredicate、IntConsumer、LongBinaryOperator、IntFunction等。Function接口还有针对输出参数类型的变种:ToIntFunction、IntToDoubleFunction等。
表3-2总结了Java API中最常用的函数式接口,它们的函数描述符及其基本类型特化。请记住这个集合只是一个启始集。如果有需要,你完全可以设计一个自己的基本类型特化(测验3.7中的TriFunction就是出于这个目的而设计的)。此外,创建你自己的接口,让接口的名字反映其在领域中的功能,还能帮助程序员理解代码逻辑,同时也便于程序的维护。请记住,标记符(T, U) -> R展示的是该怎样理解一个函数描述符。箭头左侧代表了参数的类型,右侧代表了返回结果的类型。这儿它代表的是一个函数,具有两个参数,分别为泛型T和U,返回类型为R。
表 3-2 Java 8中的常用函数式接口
| 函数式接口 |
Predicate
|
Consumer
|
|---|---|---|
Predicate
|
T -> boolean
|
|
Consumer
|
T -> void
|
|
Function
|
T -> R
|
|
Supplier
|
() -> T
|
|
UnaryOperator
|
T -> T
|
|
BinaryOperator
|
(T, T) -> T
|
|
BiPredicate
|
(T, U) -> boolean
| |
BiConsumer
|
(T, U) -> void
|
|
BiFunction
|
(T, U) -> R
|
|
你现在已经看到了很多函数式接口,可以用于描述各种Lambda表达式的签名。为了检验你的理解程度,试试测验3.4。
测验3.4:函数式接口
对于下列函数描述符(即Lambda表达式的签名),你会使用哪些函数式接口?在表3-2中可以找到大部分答案。作为进一步练习,请构造一个可以利用这些函数式接口的有效Lambda表达式:
(1)
T -> R(2)
(int, int) -> int(3)
T -> void(4)
() -> T(5)
(T, U) -> R答案:(1)
Function不错。它一般用于将类型T的对象转换为类型R的对象(比如Function用来提取苹果的重量)。(2)
IntBinaryOperator具有唯一一个抽象方法——applyAsInt,代表的函数描述符是(int, int) -> int。(3)
Consumer具有唯一一个抽象方法——accept,代表的函数描述符是T -> void。(4)
Supplier具有唯一一个抽象方法——get,代表的函数描述符是()-> T。(5)
BiFunction具有唯一一个抽象方法——apply,代表的函数描述符是(T, U) -> R。
为了总结关于函数式接口和Lambda的讨论,表3-3总结了一些使用案例、Lambda的例子,以及可以使用的函数式接口。
表 3-3 Lambda及函数式接口的例子
| 使用案例 | Lambda的例子 | 对应的函数式接口 |
|---|---|---|
| 布尔表达式 |
(List
|
Predicate
|
| 创建对象 |
() -> new Apple(10)
|
Supplier
|
| 消费一个对象 |
(Apple a) ->
System.out.println(a.getWeight())
|
Consumer
|
| 从一个对象中选择/提取 |
(String s) -> s.length()
|
Function
|
| 合并两个值 |
(int a, int b) -> a * b
|
IntBinaryOperator
|
| 比较两个对象 |
(Apple a1, Apple a2) ->
a1.getWeight().compareTo(a2.getWeight())
|
Comparator
BiFunction
|
异常、Lambda,还有函数式接口又是怎么回事?
请注意,这些函数式接口中的任何一个都不允许抛出受检异常(checked exception)。如果你需要Lambda表达式来抛出异常,有两种办法:定义一个自己的函数式接口,并声明受检异常,或者把Lambda包在一个
try/catch块中。比如,3.3节介绍过一个新的函数式接口
BufferedReaderProcessor,它显式声明了一个IOException:
@FunctionalInterfacepublic interface BufferedReaderProcessor {String process(BufferedReader b) throws IOException;}BufferedReaderProcessor p = (BufferedReader br) -> br.readLine();但是你可能是在使用一个接受函数式接口的API,比如
Function,没有办法自己创建一个(你会在下一章看到,Stream API中大量使用了表3-2中的函数式接口)。这种情况下,你可以显式捕捉受检异常:
Function<BufferedReader, String> f =(BufferedReader b) -> {try {·return b.readLine();}catch(IOException e) {throw new RuntimeException(e);}};
现在你知道如何创建Lambda,在哪里以及如何使用它们了。接下来我们会介绍一些更高级的细节:编译器如何对Lambda做类型检查,以及你应当了解的规则,诸如Lambda在自身内部引用局部变量,还有和void兼容的Lambda等。你无须立即就充分理解下一节的内容,可以留待日后再看,接着往下学习3.6节讲的方法引用就可以了。
