第 24 章:Lambda 与 Stream 实战
学习目标
- 会写 Lambda 表达式
- 掌握 Stream 链式操作
- 用 Stream 重写集合的常见处理
一、Lambda 表达式
1.1 为什么要用 Lambda?
Lambda = 匿名函数的简写。以前写一个线程:
java
// ❌ 啰嗦:匿名内部类
new Thread(new Runnable() {
@Override public void run() {
System.out.println("running");
}
}).start();
// ✅ 一行 Lambda
new Thread(() -> System.out.println("running")).start();1.2 三种写法
java
// ① 无参,() 不能省
Runnable r = () -> System.out.println("hi");
// ② 一个参数,类型可省
Consumer<String> c = s -> System.out.println(s);
// ③ 多个参数 + 方法体多行
BinaryOperator<Integer> add = (a, b) -> {
int sum = a + b;
return sum * 2;
};⚠️ 坑 1:单参数才可以省略括号,无参必须有
(),多参必须有(a, b)。java() -> 1 // ✅ x -> x * 2 // ✅ (x, y) -> x + y // ✅ x, y -> x + y // ❌
1.3 方法引用(:😃
当 Lambda 只是"调用一个现成方法"时,用 :: 更简洁:
java
// Lambda
list.forEach(s -> System.out.println(s));
// 方法引用(等价)
list.forEach(System.out::println);四种方法引用:
java
// ① 静态方法
Integer::parseInt // s -> Integer.parseInt(s)
// ② 实例方法(已知对象)
System.out::println // s -> System.out.println(s)
// ③ 实例方法(未知对象,第一个参数是调用者)
String::length // s -> s.length()
// ④ 构造方法
ArrayList::new // () -> new ArrayList()二、函数式接口(知道几个就够)
函数式接口 = 只有一个抽象方法的接口,Lambda 才能赋值给它。
java
@FunctionalInterface
interface MyFunc {
void run(); // 只有一个抽象方法
}JDK 自带的常用接口(都在 java.util.function):
| 接口 | 签名 | 用途 |
|---|---|---|
Supplier<T> | T get() | 无参,有返回值(工厂) |
Consumer<T> | void accept(T) | 有参,无返回值(消费) |
Function<T,R> | R apply(T) | 有参,有返回值(转换) |
Predicate<T> | boolean test(T) | 判断(过滤) |
Runnable | void run() | 无参无返回值(线程) |
⚠️ 坑 2:别自己造函数式接口,直接用 JDK 的
Supplier/Consumer/Function/Predicate。
三、Stream 是什么
Stream = 用链式 API 处理集合。比 for 循环更简洁。
java
// ❌ 传统写法
List<String> result = new ArrayList<>();
for (User u : users) {
if (u.getAge() >= 18) {
result.add(u.getName().toUpperCase());
}
}
// ✅ Stream 写法
List<String> result = users.stream()
.filter(u -> u.getAge() >= 18)
.map(User::getName)
.map(String::toUpperCase)
.collect(Collectors.toList());四、Stream 操作分两类
| 类型 | 方法 | 返回值 | 特点 |
|---|---|---|---|
| 中间操作 | filter / map / sorted / distinct | Stream | 链式调用,懒执行 |
| 终端操作 | collect / forEach / count / reduce | 非 Stream | 触发真正执行 |
java
// filter + map + collect:经典三连
List<String> names = users.stream()
.filter(u -> u.getActive()) // 过滤
.map(u -> u.getName()) // 转换
.sorted() // 排序
.collect(Collectors.toList()); // 收集⚠️ 坑 3:Stream 只能消费一次,用了
collect之后这个 Stream 就废了。javaStream<String> s = list.stream(); s.forEach(System.out::println); s.forEach(System.out::println); // ❌ IllegalStateException
五、常用操作大全
5.1 过滤 filter
java
List<Integer> nums = List.of(1, 2, 3, 4, 5, 6);
List<Integer> even = nums.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList()); // [2, 4, 6]5.2 转换 map
java
List<String> names = users.stream()
.map(User::getName)
.collect(Collectors.toList());5.3 排序 sorted
java
// 自然顺序
nums.stream().sorted().forEach(System.out::println);
// 自定义排序
users.stream()
.sorted(Comparator.comparing(User::getAge).reversed()) // 按年龄倒序
.forEach(System.out::println);5.4 去重 distinct
java
List<Integer> uniq = List.of(1, 2, 2, 3, 3).stream()
.distinct()
.collect(Collectors.toList()); // [1, 2, 3]5.5 截取 limit / skip
java
// 取前 10
List<User> top10 = users.stream().limit(10).collect(Collectors.toList());
// 跳过前 10,取剩下的
List<User> others = users.stream().skip(10).collect(Collectors.toList());5.6 遍历 forEach
java
list.stream().forEach(System.out::println);5.7 统计
java
long count = list.stream().count();
Optional<Integer> max = list.stream().max(Integer::compareTo);
Optional<Integer> min = list.stream().min(Integer::compareTo);
boolean any = list.stream().anyMatch(n -> n > 100); // 任一满足
boolean all = list.stream().allMatch(n -> n > 0); // 全部满足5.8 聚合 reduce
java
// 求和
int sum = nums.stream().reduce(0, Integer::sum);
// 字符串拼接
String joined = List.of("a", "b", "c").stream()
.reduce("", (a, b) -> a + b); // "abc"⚠️ 坑 4:
Optional是为了防空指针,别用if (opt.isPresent())然后get(),直接用orElse。java// ❌ 啰嗦 Integer max = list.stream().max(Integer::compareTo).isPresent() ? list.stream().max(Integer::compareTo).get() : 0; // ✅ 简洁 Integer max = list.stream().max(Integer::compareTo).orElse(0);
六、collect 收集器(最常用)
6.1 基本收集
java
// 收集为 List
List<String> list = stream.collect(Collectors.toList());
// 收集为 Set
Set<String> set = stream.collect(Collectors.toSet());
// 收集为 Map(key 不能重复!)
Map<Long, User> map = users.stream()
.collect(Collectors.toMap(User::getId, u -> u)); // id -> User6.2 分组 groupingBy
java
// 按部门分组
Map<String, List<User>> byDept = users.stream()
.collect(Collectors.groupingBy(User::getDept));
// 按部门统计人数
Map<String, Long> countByDept = users.stream()
.collect(Collectors.groupingBy(User::getDept, Collectors.counting()));6.3 分区 partitioningBy(只有 true/false 两组)
java
Map<Boolean, List<User>> parts = users.stream()
.collect(Collectors.partitioningBy(u -> u.getAge() >= 18));
// parts.get(true) -> 成年
// parts.get(false) -> 未成年6.4 拼接 joining
java
String joined = users.stream()
.map(User::getName)
.collect(Collectors.joining(", ")); // "Alice, Bob, Charlie"七、实战案例
7.1 找出年龄最大的 3 个用户
java
List<User> top3 = users.stream()
.sorted(Comparator.comparing(User::getAge).reversed())
.limit(3)
.collect(Collectors.toList());7.2 统计每个城市的用户平均年龄
java
Map<String, Double> avgAge = users.stream()
.collect(Collectors.groupingBy(
User::getCity,
Collectors.averagingInt(User::getAge)
));7.3 把 List 转 Map(处理 key 重复)
java
// ❌ key 重复会抛 IllegalStateException
Map<Long, User> map = users.stream()
.collect(Collectors.toMap(User::getId, u -> u));
// ✅ 重复时取后者
Map<Long, User> map = users.stream()
.collect(Collectors.toMap(
User::getId,
u -> u,
(existing, replacement) -> replacement));⚠️ 坑 5:
Collectors.toMap默认不允许 key 重复,生产中一定要传mergeFunction。
八、并行 Stream(慎用)
java
list.parallelStream().forEach(System.out::println);parallel 不一定快:
- ✅ 数据量大、每步独立(如统计、求和)才用
- ❌ 小数据(< 1000)反而更慢(线程切换开销)
- ❌ 涉及共享变量、有顺序要求时不能用
⚠️ 坑 6:并行 Stream 内部用
ForkJoinPool.commonPool,全局共享,不要在并行情境下阻塞。
九、本章小结
| 要点 | 关键 |
|---|---|
| Lambda | (参数) -> { 方法体 },单参数可省括号 |
| 方法引用 | 类名::方法名,Lambda 只调一个方法时用 |
| Stream | 链式操作,中间操作 + 终端操作 |
| 常用操作 | filter / map / sorted / collect |
| 收集器 | toList / toMap / groupingBy / joining |
| Optional | 用 orElse / orElseGet,别 isPresent + get |
| 并行 Stream | 数据量大 + 独立操作才用 |
动手练习
- Lambda 改写:把项目里所有匿名内部类改成 Lambda
- 过滤 + 排序:100 个用户,过滤年龄 > 18,按名字排序,取前 10
- 分组统计:用
groupingBy统计每个部门的人数和平均薪资 - List 转 Map:把
List<User>转成id -> User的 Map,处理 key 重复
下一章:第 25 章:反射基础 →