1. 深入理解函数式接口
函数式接口指的是仅包含恰好一个抽象(即未实现的)方法的接口。正因为如此,Java 才能理解:“哦,这里可以传入一个 lambda!”或者方法引用。
为避免出错,这类接口通常会加上注解 @FunctionalInterface。它并非强制,但如果你加上它却又不小心写了第二个抽象方法,编译器会立刻报错。
示例:
@FunctionalInterface
interface MyAction {
void run();
}
MyAction action = () -> System.out.println("来自 Lambda 的问候!");
action.run(); // 输出:来自 Lambda 的问候!
为什么需要它?
- 可以用 lambda 表达式或方法引用替代匿名类(更少样板代码!)。
- 让编译器明确该接口用于函数式编程。
有趣的事实: Java 标准库已经提供了数十个这样的接口——无需重复造轮子!
2. 标准函数式接口概览
在 java.util.function 包中包含数十个函数式接口。我们来看最常用的四个(它们在各类 Java 接口中“出场率”最高)。
| 接口 | 输入 | 返回 | 常见用途 |
|---|---|---|---|
|
|
|
条件判断(过滤) |
|
|
|
对对象执行操作 |
|
无 | |
获取/生成对象 |
|
|
|
将 T 转换为 R |
Predicate<T>
描述:接收类型为 T 的对象并返回 true 或 false。典型示例:列表过滤。核心方法为 test。
Predicate<String> isLong = s -> s.length() > 5;
System.out.println(isLong.test("Java")); // false
System.out.println(isLong.test("Functional")); // true
Consumer<T>
描述:接收类型为 T 的对象并对其执行操作,不返回任何值。核心方法为 accept。
Consumer<String> printer = s -> System.out.println("打印:" + s);
printer.accept("Hello, world!"); // 打印:Hello, world!
Supplier<T>
描述:不接收任何参数,返回类型为 T 的对象。可以看作“值的生成器”。核心方法为 get。
Supplier<Double> randomSupplier = () -> Math.random();
System.out.println(randomSupplier.get()); // 例如,0.1234567
Function<T, R>
描述:接收类型为 T 的对象并返回类型为 R 的对象。典型示例:数据转换。核心方法为 apply。
Function<String, Integer> stringToLength = s -> s.length();
System.out.println(stringToLength.apply("Java")); // 4
简述:UnaryOperator, BinaryOperator, BiFunction
- UnaryOperator<T> —— 等同于 Function<T, T>:输入与返回类型相同。
- BinaryOperator<T> —— 等同于 BiFunction<T, T, T>:接收两个 T,返回一个 T。
- BiFunction<T, U, R> —— 接收两种不同类型,返回第三种类型。
UnaryOperator<Integer> square = x -> x * x;
BinaryOperator<Integer> sum = (a, b) -> a + b;
BiFunction<String, Integer, String> repeat = (s, n) -> s.repeat(n);
3. 使用示例
看看这些接口在实际任务中的用法,尤其是在集合与 Stream API 中。
传给集合与 Stream API 的方法
示例 1:Predicate 与过滤
List<String> words = List.of("java", "stream", "lambda", "code");
List<String> longWords = words.stream()
.filter(word -> word.length() > 4) // Predicate<String>
.toList();
System.out.println(longWords); // [stream, lambda]
示例 2:Consumer 与 forEach
words.forEach(word -> System.out.println("单词:" + word)); // Consumer<String>
示例 3:Function 与 map
List<Integer> lengths = words.stream()
.map(word -> word.length()) // Function<String, Integer>
.toList();
System.out.println(lengths); // [4, 6, 6, 4]
示例 4:Supplier 与值生成
Supplier<String> greetingSupplier = () -> "你好,Java!";
System.out.println(greetingSupplier.get()); // 你好,Java!
与匿名类的对比
过去我们得这样写:
Predicate<String> isShort = new Predicate<String>() {
@Override
public boolean test(String s) {
return s.length() < 5;
}
};
有了 lambda,就简洁多了:
Predicate<String> isShort = s -> s.length() < 5;
4. 实践:为每个接口编写 lambda 表达式
我们实现一个小应用——用户列表。每个用户由类 User 表示:
public class User {
private final String name;
private final int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
@Override
public String toString() {
return name + " (" + age + ")";
}
}
创建用户列表:
List<User> users = List.of(
new User("Anna", 23),
new User("Boris", 17),
new User("Vika", 31),
new User("Gosha", 15)
);
Predicate:筛选成年人
Predicate<User> isAdult = user -> user.getAge() >= 18;
List<User> adults = users.stream()
.filter(isAdult)
.toList();
System.out.println("成年人: " + adults); // 成年人: [Anna (23), Vika (31)]
Consumer:打印用户
Consumer<User> printUser = user -> System.out.println("用户:" + user);
adults.forEach(printUser);
Supplier:生成用户
Supplier<User> randomUserSupplier = () -> {
String[] names = {"Dima", "Katya", "Lyosha"};
int randomAge = 10 + (int)(Math.random() * 30);
String randomName = names[(int)(Math.random() * names.length)];
return new User(randomName, randomAge);
};
User randomUser = randomUserSupplier.get();
System.out.println("随机用户:" + randomUser);
Function:获取用户名
Function<User, String> getName = user -> user.getName();
List<String> names = users.stream()
.map(getName)
.toList();
System.out.println("姓名: " + names); // 姓名: [Anna, Boris, Vika, Gosha]
5. 实用细节
在 Stream API 中的使用:filter, map, forEach 等
把这些组合起来,写一条变换管道:
users.stream()
.filter(user -> user.getAge() >= 18) // Predicate<User>
.map(user -> user.getName().toUpperCase()) // Function<User, String>
.forEach(name -> System.out.println("成年人:" + name)); // Consumer<String>
结果:
成年人: ANNA
成年人: VIKA
速查表:何时用哪个接口
| 使用场景 | 所需接口 | 使用示例 |
|---|---|---|
| filter (Stream) | |
|
| map (Stream) | |
|
| forEach (Stream, List) | |
|
| generate (Stream) | |
|
为什么要了解函数式接口?
- 它们是 Java 中所有 lambda 表达式的基础。
- 帮助你编写通用、可复用且简洁的代码。
- 简化对集合、流与异步任务的处理。
何时使用哪个接口?
- Predicate —— 需要判断条件时(过滤、查找)。
- Consumer —— 需要对对象执行某个操作时(打印、写入、发送)。
- Supplier —— 需要获取或生成对象时(工厂、生成器)。
- Function —— 需要把一种类型转换为另一种类型时。
6. 使用函数式接口的常见错误
错误 1:接口选择不当。 新手有时会把 Predicate 和 Function 混淆——例如试图从 Function 返回 boolean,而不是使用 Predicate。记住:Predicate 始终返回 boolean,Function 返回其它任意类型。
错误 2:不使用标准接口。 常见做法是自定义类似“Checker”的接口,并提供 boolean check(T t) 方法,而不是使用 Predicate。优先使用标准接口——它们得到广泛支持,也让代码对他人更易读。
错误 3:lambda 过于复杂。 如果一个 lambda 写成了 10 行的小短文,就该把它提取为单独的方法或类。lambda 追求的是简洁与可读性。
错误 4:遗忘注解 @FunctionalInterface。 如果你自定义函数式接口——别忘了这条注解。它能防止意外错误(比如多写了第二个抽象方法)。
错误 5:在 lambda 内部修改可变状态。 如果 lambda 修改了外部变量或集合,尤其在并发场景下,可能引发意料之外的 bug。应尽量避免副作用。
GO TO FULL VERSION