函数式接口定义且只定义了一个抽象方法。函数式接口很有用, 因为抽象方法的签名可以描述Lambda表达式的签名。函数式接口的抽象方法的签名称为函数描 述符。所以为了应用不同的Lambda表达式,你需要一套能够描述常见函数描述符的函数式接口。 Java API中已经有了几个函数式接口,比如Comparable、Runnable和 Callable。
Java 8的库设计师在java.util.function包中引入了几个新的函数式接口。我们接下 来会介绍Predicate、Consumer和Function,更完整的列表可见本章结尾处的表。
Predicate
java.util.function.Predicate接口定义了一个名叫test的抽象方法,它接受泛型 T对象,并返回一个boolean。这恰恰和你先前创建的一样,现在就可以直接使用了。在你需要 表示一个涉及类型T的布尔表达式时,就可以使用这个接口。比如,你可以定义一个接受String 对象的Lambda表达式,如下所示。
@FunctionalInterface
public interface Predicate<T>{
boolean test(T t);
}
public static <T> List<T> filter(List<T> list, Predicate<T> p) {
List<T> results = new ArrayList<>();
for(T s: list){
if(p.test(s)){
results.add(s);
}
}
return results;
}
Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();
List<String> nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);
Consumer
java.util.function.Consumer定义了一个名叫accept的抽象方法,它接受泛型T 的对象,没有返回(void)。你如果需要访问类型T的对象,并对其执行某些操作,就可以使用 这个接口。比如,你可以用它来创建一个forEach方法,接受一个Integers的列表,并对其中 每个元素执行操作。在下面的代码中,你就可以使用这个forEach方法,并配合Lambda来打印 列表中的所有元素。
@FunctionalInterface
public interface Consumer<T>{
void accept(T t);
}
public static <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)
);
Function
java.util.function.Function接口定义了一个叫作apply的方法,它接受一个 泛型T的对象,并返回一个泛型R的对象。如果你需要定义一个Lambda,将输入对象的信息映射 到输出,就可以使用这个接口(比如提取苹果的重量,或把字符串映射为它的长度)。在下面的 代码中,我们向你展示如何利用它来创建一个map方法,以将一个String列表映射到包含每个 String长度的Integer列表。
@FunctionalInterface
public interface Function<T, R>{
R apply(T t);
}
public static <T, R> List<R> map(List<T> list,
Function<T, R> f) {
List<R> result = new ArrayList<>();
for(T s: list){
result.add(f.apply(s));
}
return result;
}
// [7, 2, 6]
List<Integer> l = map(
Arrays.asList("lambdas","in","action"),
(String s) -> s.length()
);
原始类型特化
我们介绍了三个泛型函数式接口:Predicate、Consumer和Function。还 有些函数式接口专为某些类型而设计。
回顾一下:Java类型要么是引用类型(比如Byte、Integer、Object、List),要么是原 始类型(比如int、double、byte、char)。但是泛型(比如Consumer中的T)只能绑定到 引用类型。这是由泛型内部的实现方式造成的。①因此,在Java里有一个将原始类型转换为对应 的引用类型的机制。这个机制叫作装箱(boxing)。相反的操作,也就是将引用类型转换为对应 的原始类型,叫作拆箱(unboxing)。Java还有一个自动装箱机制来帮助程序员执行这一任务:装 箱和拆箱操作是自动完成的。比如,这就是为什么下面的代码是有效的(一个int被装箱成为 Integer):
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);
//装箱
Predicate<Integer> oddNumbers = (Integer i) -> i % 2 == 1;
oddNumbers.test(1000);
一般来说,针对专门的输入参数类型的函数式接口的名称都要加上对应的原始类型前缀,比 如DoublePredicate、IntConsumer、LongBinaryOperator、IntFunction等。Function 接口还有针对输出参数类型的变种:ToIntFunction、IntToDoubleFunction等。
下表总结了Java API中提供的最常用的函数式接口及其函数描述符。请记得这只是一个起 点。如果有需要,你可以自己设计一个。请记住,(T,U) -> R的表达方式展示了应当如何思考 一个函数描述符。表的左侧代表了参数类型。这里它代表一个函数,具有两个参数,分别为泛型 T和U,返回类型为R。
函数式接口 | 函数描述符 | 原始类型特化 |
Predicate<T> | T->boolean | IntPredicate,LongPredicate,DoublePredicate |
Consumer<T> | T->void | IntConsumer,LongConsumer,DoubleConsumer |
Function<T,R> | T->R | IntFunction<R>, |
|
| IntToDoubleFunction, |
|
| IntToLongFunction, |
|
| LongFunction<R>, |
|
| LongToDoubleFunction, |
|
| LongToIntFunction, |
|
| DoubleFunction<R>, |
|
| ToIntFunction<T>, |
|
| ToDoubleFunction<T>, |
|
| ToLongFunction<T> |
Supplier<T> | ()->T | BooleanSupplier,IntSupplier, LongSupplier, |
|
| DoubleSupplier |
UnaryOperator<T> | T->T | IntUnaryOperator, |
|
| LongUnaryOperator, |
|
| DoubleUnaryOperator |
BinaryOperator<T> | (T,T)->T | IntBinaryOperator, |
|
| LongBinaryOperator, |
|
| DoubleBinaryOperator |
BiPredicate<L,R> | (L,R)->boolean |
|
BiConsumer<T,U> | (T,U)->void | ObjIntConsumer<T>, |
|
| ObjLongConsumer<T>, |
|
| ObjDoubleConsumer<T> |
BiFunction<T,U,R> | (T,U)->R | ToIntBiFunction<T,U>, |
|
| ToLongBiFunction<T,U>, |
|
| ToDoubleBiFunction<T,U> |
为了总结关于函数式接口和Lambda的讨论,下表总结了一些使用案例、Lambda的例子,以 及可以使用的函数式接口。
使用案例 | Lambda 的例子 | 对应的函数式接口 |
布尔表达式 | (List<String> list) -> list.isEmpty() | Predicate<List<String>> |
创建对象 | () -> new Apple(10) | Supplier<Apple> |
消费一个对象 | (Apple a) ->System.out.println(a.getWeight()) | Consumer<Apple> |
从一个对象中选择/ 提取 | (String s) -> s.length() | Function<String, Integer>或ToIntFunction<String> |
合并两个值 | (int a, int b) -> a * b | IntBinaryOperator |
比较两个对象 | (Apple a1, Apple a2)->a1.getWeight().compareTo(a2.getWeight()) | Comparator<Apple>或BiFunction<Apple, Apple, Integer>或 ToIntBiFunction<Apple, Apple> |