baeldung 2025-03-27
1. 引言
本教程旨在介绍 Java 8 中提供的各种函数式接口、它们的一般使用场景,以及在 JDK 标准库中的实际应用。
2. Java 8 中的 Lambda 表达式
Java 8 引入了一项强大的语法改进:Lambda 表达式。Lambda 是一种匿名函数,可以像语言中的一等公民一样被处理,例如作为参数传递给方法,或从方法中返回。
在 Java 8 之前,每当需要封装一个单一功能时,我们通常都要创建一个类。这导致了大量不必要的样板代码,仅仅是为了表示一个基本的函数行为。
文章《Lambda 表达式与函数式接口:技巧与最佳实践》更详细地介绍了函数式接口及使用 Lambda 的最佳实践。而本文则聚焦于 java.util.function 包中一些特定的函数式接口。
3. 函数式接口
建议所有函数式接口都加上具有说明性的 @FunctionalInterface 注解。这不仅清晰地表达了接口的用途,还能让编译器在被注解的接口不满足函数式接口条件时生成错误。
任何只包含一个抽象方法(SAM, Single Abstract Method)的接口都是函数式接口,其实现可以被当作 Lambda 表达式来使用。
注意:Java 8 中的默认方法(default methods)不是抽象方法,因此不计入抽象方法数量。这意味着函数式接口可以包含多个默认方法。这一点可以从 Function 接口的文档中看出。
4. Function(函数)
最简单且通用的 Lambda 形式是一个接收一个值并返回另一个值的函数。这种单参数函数由 Function<T, R> 接口表示,其中 T 是输入类型,R 是返回类型:
public interface Function<T, R> { … }
在标准库中,Function 类型的一个典型用法是 Map.computeIfAbsent 方法。该方法根据键从 Map 中获取值;如果键不存在,则使用传入的 Function 实现计算一个新值:
Map<String, Integer> nameMap = new HashMap<>();
Integer value = nameMap.computeIfAbsent("John", s -> s.length());
在此例中,我们将对键应用函数,将结果存入 Map 并返回。我们也可以用方法引用替代 Lambda,只要其参数和返回类型匹配:
Integer value = nameMap.computeIfAbsent("John", String::length);
注意:调用方法的对象实际上是该方法的隐式第一个参数。这使得我们可以将实例方法(如 length())的引用转换为 Function 接口。
Function 接口还提供了一个默认方法 compose,用于将多个函数组合成一个,并按顺序执行:
Function<Integer, String> intToString = Object::toString;
Function<String, String> quote = s -> "'" + s + "'";
Function<Integer, String> quoteIntToString = quote.compose(intToString);
assertEquals("'5'", quoteIntToString.apply(5));
这里,quoteIntToString 先执行 intToString,再将结果传给 quote 函数。
5. 基本类型的 Function 特化
由于基本类型不能作为泛型参数,Java 为最常用的几种基本类型(double、int、long)及其组合提供了特化的 Function 接口:
- 输入为基本类型,输出为泛型:
IntFunction<R>,LongFunction<R>,DoubleFunction<R> - 输入为泛型,输出为基本类型:
ToIntFunction<T>,ToLongFunction<T>,ToDoubleFunction<T> - 输入和输出均为基本类型(名称已表明类型):
DoubleToIntFunction,IntToLongFunction,LongToDoubleFunction等
例如,Java 没有提供接收 short 并返回 byte 的内置函数式接口,但我们可以自定义:
@FunctionalInterface
public interface ShortToByteFunction {
byte applyAsByte(short s);
}
然后编写一个方法,使用该接口将 short[] 转换为 byte[]:
public byte[] transformArray(short[] array, ShortToByteFunction function) {
byte[] transformedArray = new byte[array.length];
for (int i = 0; i < array.length; i++) {
transformedArray[i] = function.applyAsByte(array[i]);
}
return transformedArray;
}
使用示例(将每个元素乘以 2):
short[] array = {(short) 1, (short) 2, (short) 3};
byte[] transformedArray = transformArray(array, s -> (byte) (s * 2));
byte[] expectedArray = {(byte) 2, (byte) 4, (byte) 6};
assertArrayEquals(expectedArray, transformedArray);
6. 双参数函数(Two-Arity)特化
要定义接受两个参数的 Lambda,需使用名称中包含 “Bi” 的接口:BiFunction, ToDoubleBiFunction, ToIntBiFunction, ToLongBiFunction。
BiFunction<T, U, R>:两个泛型参数,泛型返回值ToXXXBiFunction<T, U>:两个泛型参数,返回指定基本类型
标准 API 中的一个典型用例是 Map.replaceAll 方法,它允许用计算出的新值替换 Map 中的所有值:
Map<String, Integer> salaries = new HashMap<>();
salaries.put("John", 40000);
salaries.put("Freddy", 30000);
salaries.put("Samuel", 50000);
salaries.replaceAll((name, oldValue) ->
name.equals("Freddy") ? oldValue : oldValue + 10000);
这里,Lambda 接收键(姓名)和旧值(薪资),并返回新薪资。
7. Supplier(供给者)
Supplier<T> 是另一种 Function 的特化形式,不接收任何参数,仅返回一个值。常用于惰性求值(lazy evaluation)。
例如,定义一个平方函数,其参数通过 Supplier<Double> 提供:
public double squareLazy(Supplier<Double> lazyValue) {
return Math.pow(lazyValue.get(), 2);
}
这样,只有在调用 lazyValue.get() 时才会真正计算参数值。若计算耗时较长(如模拟 1 秒延迟):
Supplier<Double> lazyValue = () -> {
Uninterruptibles.sleepUninterruptibly(1000, TimeUnit.MILLISECONDS);
return 9d;
};
Double valueSquared = squareLazy(lazyValue);
另一个常见用途是序列生成。例如,使用 Stream.generate 生成斐波那契数列:
int[] fibs = {0, 1};
Stream<Integer> fibonacci = Stream.generate(() -> {
int result = fibs[1];
int fib3 = fibs[0] + fibs[1];
fibs[0] = fibs[1];
fibs[1] = fib3;
return result;
});
此处传给 Stream.generate 的 Lambda 实现了 Supplier<Integer>。注意:为了在 Lambda 中修改状态,我们使用数组(而非普通变量),因为 Lambda 中使用的外部变量必须是“有效 final”(effectively final)。
此外还有针对基本类型的特化版本:BooleanSupplier, DoubleSupplier, IntSupplier, LongSupplier。
8. Consumer(消费者)
与 Supplier 相反,Consumer<T> 接收一个泛型参数,不返回任何值,通常用于表示副作用(side effects)。
例如,遍历名字列表并打印问候语:
List<String> names = Arrays.asList("John", "Freddy", "Samuel");
names.forEach(name -> System.out.println("Hello, " + name));
这里的 Lambda 实现了 Consumer<String>。
也有针对基本类型的特化:DoubleConsumer, IntConsumer, LongConsumer。
更有趣的是 BiConsumer<T, U>,常用于遍历 Map 的条目:
Map<String, Integer> ages = new HashMap<>();
ages.put("John", 25);
ages.put("Freddy", 24);
ages.put("Samuel", 30);
ages.forEach((name, age) -> System.out.println(name + " is " + age + " years old"));
此外还有混合泛型与基本类型的 BiConsumer 特化:ObjDoubleConsumer<T>, ObjIntConsumer<T>, ObjLongConsumer<T>。
9. Predicate(断言)
在数理逻辑中,谓词(Predicate) 是一个接收值并返回布尔值的函数。
Predicate<T> 是 Function<T, Boolean> 的特化,常用于过滤集合:
List<String> names = Arrays.asList("Angela", "Aaron", "Bob", "Claire", "David");
List<String> namesWithA = names.stream()
.filter(name -> name.startsWith("A"))
.collect(Collectors.toList());
此处的 Lambda 实现了 Predicate<String>,封装了过滤逻辑。
同样,也有针对基本类型的版本:IntPredicate, LongPredicate, DoublePredicate。
10. Operator(操作符)
操作符是输入与输出类型相同的函数。
UnaryOperator<T>:接收一个T,返回一个T
例如,在List.replaceAll中将所有字符串转为大写:List<String> names = Arrays.asList("bob", "josh", "megan"); names.replaceAll(String::toUpperCase);此处的 Lambda 必须返回与输入相同类型的值,因此
UnaryOperator非常合适。BinaryOperator<T>:接收两个T,返回一个T
典型用例是归约(reduction)操作,例如求和:List<Integer> values = Arrays.asList(3, 5, 8, 9, 12); int sum = values.stream().reduce(0, (i1, i2) -> i1 + i2);reduce方法接收初始值和一个BinaryOperator。该函数必须满足结合律(associative):op.apply(a, op.apply(b, c)) == op.apply(op.apply(a, b), c)结合律使得归约操作可以安全地并行化。
同样,也提供了基本类型特化版本:IntUnaryOperator, LongBinaryOperator, DoubleUnaryOperator 等。
11. 旧版函数式接口
并非所有函数式接口都是 Java 8 新增的。许多旧版接口也符合函数式接口的要求,可作为 Lambda 使用。
典型例子包括并发 API 中的 Runnable 和 Callable。在 Java 8 中,它们也被加上了 @FunctionalInterface 注解,从而大大简化并发代码:
Thread thread = new Thread(() -> System.out.println("Hello From Another Thread"));
thread.start();
12. 结论
本文介绍了 Java 8 API 中的各种函数式接口,以及如何将它们用作 Lambda 表达式。这些接口为函数式编程提供了强大而简洁的基础,广泛应用于集合处理、流操作、并发编程等场景。