引言
为什么要用Stream
- 可读性高:把"做什么"与"怎么做"区分开,代码更接近业务描述(声明式)。
- 可组合:操作可以链式组合,减少样板代码(boilerplate)。
- 惰性与优化:中间操作惰性求值,可实现短路与合并优化。
- 并行化支持 :调用
parallel()或parallelStream()可以较容易地并行处理数据(需注意线程安全)。- 功能强:内置聚合、分组、统计、收集等能力,非常适合处理集合数据。
但也有代价:不当使用会损失性能、产生线程安全问题或破坏可预测性(例如在并行流中使用有状态的副作用操作)。
Stream 背景与特点(简要)
- 来源:Stream 不是数据结构,不存储元素;它是对数据源(Collection、数组、I/O channel 等)的一次性操作流水线。
- 惰性求值 :中间操作(
filter,map等)仅构建操作链;终端操作(collect,forEach等)触发计算。 - 可重用性限制:同一个 Stream 只能用一次(一次终端操作后关闭)。要重复操作,需要创建新的 Stream。
- 分为中间操作(返回 Stream)和终端操作(返回结果/void)。
- 支持顺序/并行执行 :
stream()(顺序)与parallelStream()(并行)或stream().parallel()。
如何创建 Stream(常见方式)
java
List<String> list = List.of("a", "b", "c");
// 1. 从集合
Stream<String> s1 = list.stream();
// 2. 从并行集合
Stream<String> s2 = list.parallelStream();
// 3. 从数组
Stream<Integer> s3 = Arrays.stream(new Integer[]{1,2,3});
// 4. 从静态工厂
Stream<String> s4 = Stream.of("x", "y", "z");
// 5. 空流
Stream<Object> empty = Stream.empty();
// 6. 无界流(生成、迭代)
Stream<Double> randoms = Stream.generate(Math::random); // 无尽流,通常再 limit()
Stream<Integer> iter = Stream.iterate(0, n -> n + 2);
// 7. 从文件/IO(NIO)
try (Stream<String> lines = Files.lines(Paths.get("data.txt"))) {
lines.forEach(System.out::println);
}
Stream 的中间操作(常见)与代码示例
中间操作返回新的 Stream,通常是惰性的。
1) filter(过滤)
java
List<Person> people = ...;
List<Person> adults = people.stream()
.filter(p -> p.getAge() >= 18)
.collect(Collectors.toList());
filter 保持元素顺序(对有序流)。
2) map(映射) & flatMap
java
List<String> names = people.stream()
.map(Person::getName)
.collect(Collectors.toList());
// flatMap 示例:将 List<List<T>> 展平
List<List<String>> nested = List.of(List.of("a","b"), List.of("c"));
List<String> flat = nested.stream()
.flatMap(Collection::stream)
.collect(Collectors.toList());
3) distinct(去重)
基于 equals/hashCode。
java
List<Integer> unique = List.of(1,2,2,3).stream().distinct().collect(Collectors.toList()); // [1,2,3]
4) sorted(排序)
有序流或无序流均可使用。默认使用自然顺序或提供 Comparator。
java
people.stream().sorted(Comparator.comparing(Person::getAge).reversed()).collect(...);
5) peek(调试/副作用)
用于调试或中间副作用(谨慎使用)。
java
people.stream().filter(...).peek(System.out::println).collect(...);
6) limit / skip(截断、跳过)
短路友好,用于分页或流式处理。
java
stream.skip(10).limit(20)...
7) parallel(并行化)与 sequential
java
stream.parallel()... // 切为并行模式
stream.sequential()... // 切回顺序模式
终端操作(收集、聚合、匹配、查找)
遍历(forEach / forEachOrdered)
java
stream.forEach(System.out::println); // 并行流可能无顺序
stream.forEachOrdered(System.out::println); // 保证顺序(代价更高)
匹配操作 anyMatch / allMatch / noneMatch
短路操作(遇到条件就停止)。
java
boolean hasMinor = people.stream().anyMatch(p -> p.getAge() < 18);
查找 findFirst / findAny
返回 Optional<T>。
java
Optional<Person> first = people.stream().filter(...).findFirst();
规约(reduce)
通用的归约操作。
java
// 求和
int sum = List.of(1,2,3).stream().reduce(0, Integer::sum);
// 返回 Optional
Optional<Integer> maybe = List.of(1,2,3).stream().reduce(Integer::sum);
聚合(count, max, min)
java
long cnt = people.stream().filter(...).count();
Optional<Person> oldest = people.stream().max(Comparator.comparing(Person::getAge));
收集(collect + Collectors)
collect 是最强大的终端操作:
常见 Collector 用法
java
// toList / toSet / toMap
List<String> names = people.stream().map(Person::getName).collect(Collectors.toList());
Map<Integer, List<Person>> byAge = people.stream().collect(Collectors.groupingBy(Person::getAge));
Map<Integer, String> map = people.stream()
.collect(Collectors.toMap(Person::getId, Person::getName)); // 若 key 重复需提供 merge 函数
// joining(字符串连接)
String csv = people.stream().map(Person::getName).collect(Collectors.joining(", "));
// summarizing(统计)
IntSummaryStatistics stats = people.stream().collect(Collectors.summarizingInt(Person::getAge));
// stats.getCount(), getSum(), getAverage(), getMax(), getMin()
// averaging / summing
double avg = people.stream().collect(Collectors.averagingInt(Person::getAge));
int total = people.stream().collect(Collectors.summingInt(Person::getAge));
分组与分区(核心示例与进阶)
假设有 Order 类:
java
record Order(long id, long userId, int amount, String status) {}
List<Order> orders = List.of(
new Order(1, 100, 50, "PAID"),
new Order(2, 101, 20, "CREATED"),
new Order(3, 100, 70, "PAID"),
new Order(4, 102, 30, "REFUND")
);
- 按 userId 分组
java
Map<Long, List<Order>> byUser = orders.stream()
.collect(Collectors.groupingBy(Order::userId));
- 分组后求和/统计
java
Map<Long, Integer> amountByUser = orders.stream()
.collect(Collectors.groupingBy(Order::userId,
Collectors.summingInt(Order::amount)));
- 多级分组(先 status 再 user)
java
Map<String, Map<Long, List<Order>>> multi =
orders.stream().collect(Collectors.groupingBy(Order::status,
Collectors.groupingBy(Order::userId)));
- 分区(partitioningBy) ------ 返回两个桶(true/false)
java
Map<Boolean, List<Order>> bigOrSmall = orders.stream()
.collect(Collectors.partitioningBy(o -> o.amount > 50));
5) groupingByConcurrent(并发分组)
用于并行流时减少锁竞争:
java
ConcurrentMap<Long, Integer> concurrentSum = orders.parallelStream()
.collect(Collectors.groupingByConcurrent(Order::userId, Collectors.summingInt(Order::amount)));
进阶:Collectors 的常用组合模式
TopN(分组后取 top k)
java
Map<Long, List<Order>> top2ByUser = orders.stream()
.collect(Collectors.groupingBy(Order::userId,
Collectors.collectingAndThen(
Collectors.toList(),
list -> list.stream()
.sorted(Comparator.comparingInt(Order::amount).reversed())
.limit(2)
.collect(Collectors.toList())
)));
toMap 带合并函数(防止 key 冲突)
java
Map<Long, Integer> totalByUser = orders.stream()
.collect(Collectors.toMap(Order::userId, Order::amount, Integer::sum));
原始类型流(IntStream / LongStream / DoubleStream)
避免自动拆装箱,提升性能/内存效率。
java
int total = orders.stream().mapToInt(Order::amount).sum();
IntSummaryStatistics st = orders.stream().mapToInt(Order::amount).summaryStatistics();
从数组快速创建:
java
IntStream.range(0, 10).forEach(System.out::println);
IntStream.rangeClosed(1, 5).toArray(); // 包含端点
并行流(parallelStream)与性能/线程安全注意事项
并行流能利用多核,但要注意:
- 要确保操作是无副作用、线程安全的(例如避免对共享集合做非线程安全的 add)。
- 慎用
forEach在并行流中对外部 mutable 状态产生副作用。 - 并行不是总是快:小集合、短链式操作、存在顺序依赖(ordered)或排序/limit/skip 等操作,可能导致开销更大。
- 使用并发收集器 (如
groupingByConcurrent)以提高并行性能。 Collectors.toList()在并行流下不是线程安全的聚合器,会由框架内部合并片段,但外部直接对同一 list 操作会出事。
示例并发安全写法:
java
Map<Long, Integer> concurrent = orders.parallelStream()
.collect(Collectors.groupingByConcurrent(Order::userId, Collectors.summingInt(Order::amount)));
常见陷阱与调优建议
- 不要在流中做阻塞 IO (如在
map中调用网络请求),会阻塞线程池。 - 注意短路与惰性 :把易短路的操作放在前面(例如高选择性
filter放在早期,减少后续工作量)。 - 避免在并行流中使用非线程安全的集合做副作用收集 。使用
collect而不是外部add。 - 对大量数据做排序/聚合可能耗内存,考虑流式分批或外部排序/外部聚合。
- 使用
mapToInt/mapToLong以避免装箱成本。 - 使用合适的 Collector :
groupingByConcurrent对并行流友好,toMap要处理 key 冲突。 - 限制并行度 :可以设置
ForkJoinPool.commonPool()的并行度或用自定义 ForkJoinPool 执行并行流任务,谨慎使用全局修改。
实战完整示例 ------ 一个中等复杂度的任务
需求:给定订单列表,统计每个 userId 的订单总额、最近订单时间,并取 top3 用户按总额排序。
java
import java.time.Instant;
import java.util.*;
import java.util.stream.*;
record Order(long id, long userId, int amount, Instant ts){}
public class StreamExample {
public static void main(String[] args) {
List<Order> orders = List.of(
new Order(1, 100, 50, Instant.parse("2025-01-01T10:00:00Z")),
new Order(2, 101, 20, Instant.parse("2025-01-02T10:00:00Z")),
new Order(3, 100, 70, Instant.parse("2025-01-03T10:00:00Z")),
new Order(4, 102, 30, Instant.parse("2025-01-01T09:00:00Z")),
new Order(5, 101, 200, Instant.parse("2025-01-04T10:00:00Z"))
);
// 分组并计算总额与最近时间
Map<Long, UserStat> stats = orders.stream()
.collect(Collectors.groupingBy(Order::userId, Collectors.collectingAndThen(
Collectors.toList(),
list -> {
int total = list.stream().mapToInt(Order::amount).sum();
Instant last = list.stream().map(Order::ts).max(Comparator.naturalOrder()).orElse(Instant.EPOCH);
return new UserStat(total, last);
}
)));
// top3 用户
List<Map.Entry<Long, UserStat>> top3 = stats.entrySet().stream()
.sorted(Map.Entry.<Long, UserStat>comparingByValue(Comparator.comparingInt(UserStat::total).reversed()))
.limit(3)
.collect(Collectors.toList());
top3.forEach(e -> System.out.println(e.getKey() + " -> " + e.getValue()));
}
static class UserStat {
int total;
Instant last;
UserStat(int total, Instant last) { this.total = total; this.last = last; }
int total() { return total; }
Instant last() { return last; }
public String toString() { return "total=" + total + ", last=" + last; }
}
}
这段代码展示了:分组(groupingBy)→ 聚合(summing / max)→ 排序 → topN 的完整流水线。
总结
- Stream 提供强大、声明式的数据处理能力,能显著简化集合操作的代码量与可读性。
- 常用操作:过滤(
filter)、映射(map)、归约(reduce)、收集(collect)、分组(groupingBy)等。 - 并行流是便捷的加速工具,但需要理解线程安全、合并成本与适用场景。
- 熟练运用
Collectors(复合收集器、分组/并发收集、下游收集器)是写好 Stream 代码的关键。 - 在生产环境,注意避免在流中做阻塞 IO、不安全的副作用和不恰当的排序/深度聚合导致内存问题。