1. 传递行为而不是数据
为什么要把函数作为参数传递?
设想:你有一个用来排序列表的方法。但它怎么知道要按什么规则排序呢?按字母?按长度?按生日?你可以为每种情况都写一个单独的方法,但这很快就会变成疯狂的复制粘贴。
相反,Java 允许你传递行为(也就是函数或 lambda),由它来决定如何排序、过滤、转换等等。这样代码就更灵活、可复用。
示例:使用比较器排序
List<String> names = List.of("Anna", "Boris", "Vika");
// 传递行为:如何比较元素(按字符串长度)
names.stream()
.sorted((a, b) -> a.length() - b.length())
.forEach(System.out::println);
示例:使用谓词过滤
List<String> names = List.of("Anna", "Boris", "Vika");
// 传递行为:保留谁(名字长度大于 4)
names.stream()
.filter(name -> name.length() > 4)
.forEach(System.out::println);
在这两种情况下,你都没有把逻辑“硬编码”到方法里,而是给了它一小段行为,它会应用到每个元素上。
好处:更少重复,更高灵活性
不必写十几个代码相近但逻辑不同的方法,你只需写一个通用方法,把“该做什么”作为函数传进去。这能节省时间、减少错误,也让代码更易于测试。
2. 传递函数的语法
将 lambda 表达式作为参数
最常见的方式——在调用方法的地方直接写一个带箭头 -> 的 lambda:
list.forEach(item -> System.out.println(item));
甚至还能更短,如果已经有合适的方法——使用方法引用(::):
list.forEach(System.out::println);
方法引用(method references)
如果已经有合适的方法,可以用 :: 传递它的引用:
// 普通方法
public static boolean isLongName(String name) {
return name.length() > 4;
}
// 传递方法引用
names.stream()
.filter(MyClass::isLongName)
.forEach(System.out::println);
这在方法签名与目标函数式接口期望的签名一致时成立(如 Predicate<T>、Function<T, R>、Comparator<T> 等)。
3. 来自标准库的示例
Collections.sort 与 Comparator
List<String> names = new ArrayList<>(List.of("Anna", "Boris", "Vika"));
// 按名字长度排序
names.sort((a, b) -> a.length() - b.length());
System.out.println(names); // [Vika, Anna, Boris]
Stream.filter 与 Predicate
List<String> names = List.of("Anna", "Boris", "Vika");
// 只保留以 'V' 开头的名字
names.stream()
.filter(name -> name.startsWith("V"))
.forEach(System.out::println); // Vika
Stream.map 与 Function
List<String> names = List.of("Anna", "Boris", "Vika");
// 将名字转换为大写
names.stream()
.map(String::toUpperCase)
.forEach(System.out::println);
Optional.ifPresent 与 Consumer
Optional<String> opt = Optional.of("你好!");
// 如果有值,就打印出来
opt.ifPresent(s -> System.out.println("在字符串中:" + s));
4. 实战:编写以函数为参数的方法
是时候把知识用起来了!我们来写几个方法,它们接收一个函数作为参数并在内部使用它。
示例 1:处理列表元素的方法(自制 forEach)
假设我们有一个用户列表(User)。我们想对每个用户执行某个动作——例如打印名字、发送邮件、发放积分等。不把动作“硬编码”,而是把它作为参数传入——Consumer<User>!
import java.util.List;
import java.util.function.Consumer;
class User {
String name;
int age;
User(String name, int age) {
this.name = name;
this.age = age;
}
}
public class UserProcessor {
// 接收 Consumer<User> 的方法
public static void processUsers(List<User> users, Consumer<User> action) {
for (User user : users) {
action.accept(user);
}
}
public static void main(String[] args) {
List<User> users = List.of(
new User("Anna", 25),
new User("Boris", 30),
new User("Vika", 22)
);
// 打印所有用户的姓名
processUsers(users, user -> System.out.println("姓名:" + user.name));
// 发放奖励(示例中仅打印)
processUsers(users, user -> System.out.println(user.name + " 获得了奖励!"));
}
}
示例 2:过滤元素的方法(Predicate)
编写一个方法,只返回满足条件的用户(例如,只要成年人):
import java.util.List;
import java.util.ArrayList;
import java.util.function.Predicate;
public class UserFilter {
public static List<User> filterUsers(List<User> users, Predicate<User> condition) {
List<User> result = new ArrayList<>();
for (User user : users) {
if (condition.test(user)) {
result.add(user);
}
}
return result;
}
public static void main(String[] args) {
List<User> users = List.of(
new User("Anna", 25),
new User("Boris", 17),
new User("Vika", 22)
);
// 仅过滤成年人
List<User> adults = filterUsers(users, user -> user.age >= 18);
adults.forEach(user -> System.out.println(user.name)); // Anna, Vika
}
}
示例 3:转换方法(Function)
现在编写一个方法,把用户列表转换为他们的姓名列表(或任何其它转换):
import java.util.List;
import java.util.ArrayList;
import java.util.function.Function;
public class UserMapper {
public static <R> List<R> mapUsers(List<User> users, Function<User, R> mapper) {
List<R> result = new ArrayList<>();
for (User user : users) {
result.add(mapper.apply(user));
}
return result;
}
public static void main(String[] args) {
List<User> users = List.of(
new User("Anna", 25),
new User("Boris", 17),
new User("Vika", 22)
);
// 获取姓名列表
List<String> names = mapUsers(users, user -> user.name);
System.out.println(names); // [Anna, Boris, Vika]
// 获取年龄列表
List<Integer> ages = mapUsers(users, user -> user.age);
System.out.println(ages); // [25, 17, 22]
}
}
示例 4:生成方法(Supplier)
有时需要“按需”获取某个值——例如生成随机数、创建对象、获取当前时间。这时适合使用接口 Supplier<T>。
import java.util.function.Supplier;
public class ValueGenerator {
public static int getValue(Supplier<Integer> supplier) {
return supplier.get();
}
public static void main(String[] args) {
// 获取随机数
int random = getValue(() -> (int)(Math.random() * 100));
System.out.println("随机数:" + random);
// 获取当前时间(毫秒)
long time = getValue(System::currentTimeMillis);
System.out.println("时间:" + time);
}
}
5. 一个完整的小应用:把示例串起来
假设我们要做一个简单的“用户列表”应用。我们已经会:
- 按条件过滤用户(例如只要成年人);
- 把用户转换成其他东西(姓名、邮箱、年龄);
- 对每个用户执行动作(打印、发放积分);
- 按需生成值(例如创建新用户)。
现在使用这些方法,我们就可以构建灵活的数据处理方案,无需每次为新需求重写代码。
import java.util.*;
import java.util.function.*;
public class UserApp {
static class User {
String name;
int age;
User(String name, int age) {
this.name = name;
this.age = age;
}
public String toString() {
return name + " (" + age + ")";
}
}
// 通用的用户处理方法
static void processUsers(List<User> users, Consumer<User> action) {
for (User user : users) action.accept(user);
}
// 通用过滤器
static List<User> filterUsers(List<User> users, Predicate<User> condition) {
List<User> result = new ArrayList<>();
for (User user : users) if (condition.test(user)) result.add(user);
return result;
}
// 通用转换器
static <R> List<R> mapUsers(List<User> users, Function<User, R> mapper) {
List<R> result = new ArrayList<>();
for (User user : users) result.add(mapper.apply(user));
return result;
}
public static void main(String[] args) {
List<User> users = List.of(
new User("Anna", 25),
new User("Boris", 17),
new User("Vika", 22)
);
// 1. 打印所有用户
processUsers(users, user -> System.out.println("用户:" + user));
// 2. 只找出成年人
List<User> adults = filterUsers(users, user -> user.age >= 18);
System.out.println("成年人:" + adults);
// 3. 获取成年人的姓名
List<String> adultNames = mapUsers(adults, user -> user.name);
System.out.println("成年人的姓名:" + adultNames);
// 4. 给成年人发放奖励
processUsers(adults, user -> System.out.println(user.name + " 获得了奖励!"));
}
}
为什么这比“传统”做法更好?
这种方式更灵活:同一组方法可以用于完全不同的场景,只需传入不同的函数即可。代码不会膨胀成无穷无尽的“这样过滤”、“那样打印”——我们有一套通用的工具,处处可用。因此代码更紧凑、清晰,也更不容易出错。而且这种风格与现代的 Stream API 完美契合,迁移到流式处理时无需重新学习——思路完全一致。
6. 将函数作为参数传递时的常见错误
错误 1:签名与期望的接口不匹配。
如果方法接收 Predicate<User>,而你传入的 lambda 返回的不是 boolean,而是例如 String,编译器会立刻说“哎呀呀!”并拒绝构建项目。请检查返回值与参数是否与期望一致。
错误 2:lambda 使用了可能会变化的变量。
在 Java 中,lambda 表达式只能使用外部上下文中的 final 或“有效 final(effectively final)”变量。如果你试图在 lambda 中修改这样的变量——会产生编译错误。
错误 3:混淆了接口类型。
有时你想传递的是 Consumer<T>,却写成了有返回值的函数(例如 Function<T, R>)。请确认你的 lambda 返回的正是所需的内容(或什么都不返回——对于 Consumer)。
错误 4:把过于复杂的 lambda 直接写在参数里。
如果一个 lambda 超过一两行,最好把它提取成单独的变量或方法。否则代码会变得难以阅读,最后只有你能看懂(而且未必总能看懂)。
GO TO FULL VERSION