Java 中的函数式接口

更新于 2025-12-29

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 为最常用的几种基本类型(doubleintlong)及其组合提供了特化的 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 中的 RunnableCallable。在 Java 8 中,它们也被加上了 @FunctionalInterface 注解,从而大大简化并发代码:

Thread thread = new Thread(() -> System.out.println("Hello From Another Thread"));
thread.start();

12. 结论

本文介绍了 Java 8 API 中的各种函数式接口,以及如何将它们用作 Lambda 表达式。这些接口为函数式编程提供了强大而简洁的基础,广泛应用于集合处理、流操作、并发编程等场景。