Java 8 把 Stream 加进来之后,处理集合的方式跟以前不太一样了。你可能也写过 list.stream().filter(...).collect(...),但中间操作、终止操作、收集器这几个东西搅在一起时,还是容易写错。
这篇把 Java 8 里常用到的 Stream 用法过一遍,每个都配上能直接跑的例子。并行流和常见的坑也一并说一下。
一、Stream 是什么
先说它不存数据这件事:
- 它不是集合,只是数据的管道。
- 它没有下标,不能随机访问。
- 它不动源数据,每次操作都产生新结果。
一条 Stream 跑起来就是三步:
1
| 数据源 → 中间操作(0~N个) → 终止操作(1个)
|
举个最基础的例子:
1 2 3 4 5 6
| List<String> names = Arrays.asList("Alice", "Bob", "Charlie"); List<String> result = names.stream() .filter(n -> n.length() > 3) .map(String::toUpperCase) .collect(Collectors.toList());
|
有两个地方容易被坑:
- 惰性求值:filter、map 这些中间操作不会马上执行,得等 collect、count 这种终止操作出现,整条链才真正跑。
- 一次性:一个 Stream 对象只能被消费一次。第二次调用终止操作会抛
IllegalStateException: stream has already been operated upon or closed。
二、怎么建一个 Stream
数据源来源挺多的,常用的就这几类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| List<String> list = Arrays.asList("a", "b", "c"); Stream<String> s1 = list.stream(); Stream<String> s2 = list.parallelStream();
String[] arr = {"a", "b", "c"}; Stream<String> s3 = Arrays.stream(arr);
Stream<String> s4 = Stream.of("a", "b", "c");
Stream<Integer> s5 = Stream.iterate(0, n -> n + 2).limit(5);
Stream<Double> s6 = Stream.generate(Math::random).limit(3);
IntStream s7 = IntStream.range(0, 5); IntStream s8 = IntStream.rangeClosed(0, 5);
try (Stream<String> lines = Files.lines(Paths.get("data.txt"))) { lines.forEach(System.out::println); }
Stream<String> s9 = Stream.empty();
|
三、中间操作
中间操作返回的是新 Stream,而且是惰性的:不碰到终止操作,它什么都不干。
无状态操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| stream.filter(x -> x > 0)
stream.map(String::toUpperCase) stream.mapToInt(Integer::intValue)
List<List<Integer>> nested = Arrays.asList(Arrays.asList(1, 2), Arrays.asList(3, 4)); List<Integer> flat = nested.stream() .flatMap(List::stream) .collect(Collectors.toList());
List<String> words = lines.stream() .flatMap(line -> Arrays.stream(line.split(" "))) .collect(Collectors.toList());
stream.peek(System.out::println)
|
有状态操作
1 2 3 4 5 6 7 8 9 10 11 12
| stream.distinct()
stream.sorted() stream.sorted(Comparator.comparing(Person::getAge).reversed())
stream.limit(10)
stream.skip(5)
|
sorted、distinct 这类有状态操作,在并行流里需要各线程之间同步状态,开销不小。数据量不大的时候,别为了好看硬上并行。
四、终止操作
终止操作会真正触发整条链执行,而且一条流上只能有一个。
遍历与收集
1 2 3 4 5 6 7 8 9 10
| stream.forEach(System.out::println);
stream.parallel().forEachOrdered(System.out::println);
List<String> list = stream.collect(Collectors.toList());
String[] arr = stream.toArray(String[]::new);
|
聚合与归约
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| long n = stream.count();
int sum1 = Arrays.asList(1, 2, 3, 4).stream().reduce(0, Integer::sum);
Optional<Integer> sum2 = Arrays.asList(1, 2, 3, 4).stream().reduce(Integer::sum);
int totalLen = Arrays.asList("ab", "cde", "fg").stream().reduce( 0, (len, word) -> len + word.length(), Integer::sum);
|
关于第三个重载的 combiner:串行执行时它不参与计算;一旦 .parallel(),流会被切成多段,每段各自用 accumulator 算出局部结果,最后用 combiner 把各段结果合成一个。所以 combiner 不是装饰,是并行正确性的保证。
有个坑得留意:初始值(identity)必须是”中性值”,即满足 combiner.apply(identity, x) == x。求和用 0、求积用 1、拼接用 ""。如果求和误把初始值写成 10,并行时每个分片都从 10 起步,合并后会多出”分片数个 10”,结果就错了。
reduce 与 collect 的分工:reduce 适合”聚合成一个标量或简单对象”(数字、字符串、自定义累加器);”收集成集合 / 分组 / 转 Map”请用 collect + Collectors,那是它的主场,别硬用 reduce 拼 List(既慢又啰嗦)。
匹配与查找
1 2 3 4 5 6 7 8
| boolean any = stream.anyMatch(x -> x > 10); boolean all = stream.allMatch(x -> x > 0); boolean none = stream.noneMatch(x -> x < 0);
Optional<String> first = stream.findFirst(); Optional<String> anyOne = stream.findAny();
|
极值
1 2 3 4
| Optional<Integer> max = numbers.stream().max(Integer::compareTo); Optional<Integer> min = numbers.stream().min(Integer::compareTo);
int maxVal = numbers.stream().mapToInt(Integer::intValue).max().orElse(0);
|
五、Collectors 收集器
collect 配 Collectors 基本上包揽了所有的收尾工作,也是平时写 Stream 用得最频繁的一块。
收集到集合
1 2 3 4 5 6 7 8 9 10
| List<String> list = stream.collect(Collectors.toList()); Set<String> set = stream.collect(Collectors.toSet());
Set<String> linked = stream.collect(Collectors.toCollection(LinkedHashSet::new));
List<String> immutable = Collections.unmodifiableList( stream.collect(Collectors.toList()));
|
转 Map
转 Map 是最容易踩坑的地方:
1 2 3 4 5 6 7 8 9 10 11
| Map<String, Integer> m1 = people.stream() .collect(Collectors.toMap(Person::getName, Person::getAge));
Map<String, Integer> m2 = people.stream() .collect(Collectors.toMap(Person::getName, Person::getAge, (oldV, newV) -> newV));
Map<String, Person> m3 = people.stream() .collect(Collectors.toMap(Person::getName, Function.identity()));
|
分组与分区
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| Map<Integer, List<Person>> byAge = people.stream() .collect(Collectors.groupingBy(Person::getAge));
Map<Integer, Map<String, List<Person>>> byAgeThenName = people.stream() .collect(Collectors.groupingBy(Person::getAge, Collectors.groupingBy(Person::getName)));
Map<Integer, Long> countByAge = people.stream() .collect(Collectors.groupingBy(Person::getAge, Collectors.counting()));
Map<Integer, Double> avgScore = students.stream() .collect(Collectors.groupingBy(Student::getClassId, Collectors.averagingInt(Student::getScore)));
Map<Boolean, List<Person>> adults = people.stream() .collect(Collectors.partitioningBy(p -> p.getAge() >= 18));
|
统计与拼接
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| long count = stream.collect(Collectors.counting()); double avg = stream.collect(Collectors.averagingInt(Person::getAge)); int sum = stream.collect(Collectors.summingInt(Person::getAge));
IntSummaryStatistics stat = people.stream() .collect(Collectors.summarizingInt(Person::getAge));
Optional<Person> oldest = people.stream() .collect(Collectors.maxBy(Comparator.comparing(Person::getAge)));
String joined = stream.collect(Collectors.joining()); String csv = stream.collect(Collectors.joining(", ")); String pretty = stream.collect(Collectors.joining(", ", "[", "]"));
|
进阶:mapping 与 collectingAndThen
1 2 3 4 5 6 7 8 9
| Map<Integer, List<String>> namesByAge = people.stream() .collect(Collectors.groupingBy(Person::getAge, Collectors.mapping(Person::getName, Collectors.toList())));
List<String> safe = stream.collect(Collectors.collectingAndThen( Collectors.toList(), Collections::unmodifiableList));
|
六、Optional 与 Stream 的配合
凡是”可能不存在”的结果(findFirst、max、min、reduce 无初值版),返回的都是 Optional。别再用 null 到处判空了:
1 2 3 4 5 6 7 8
| Optional<Person> top = people.stream() .max(Comparator.comparing(Person::getScore));
top.ifPresent(p -> System.out.println(p.getName())); Person p1 = top.orElse(defaultPerson); Person p2 = top.orElseGet(() -> loadDefault()); Person p3 = top.orElseThrow(() -> new RuntimeException("没人"));
|
七、并行流
一行 parallel() 就把串行流切到并行:
1 2 3 4 5 6
| long count = list.parallelStream() .filter(s -> s.length() > 3) .count();
list.stream().parallel().filter(...).sequential().map(...);
|
底层用 ForkJoinPool.commonPool(),把数据分片,多个线程各算一段,最后合并。
适合用并行的情况:
- 数据量大,得上万、百万级才能看出收益。
- 每个元素的计算是 CPU 密集且互相独立的。
- 中间没有
sorted、distinct 这类需要跨线程同步状态的操作,否则并行优势基本被抵消。
不适合的情况:
- 数据量小,线程调度的开销比计算本身还大。
- IO 密集(网络、数据库),会占满公共线程池,拖垮整个 JVM。
- 操作里碰了共享可变状态,并行读写不加锁就是 bug。
1 2 3 4 5 6 7
| List<Integer> nums = ...; int[] sum = {0}; nums.parallelStream().forEach(n -> sum[0] += n);
int total = nums.parallelStream().mapToInt(Integer::intValue).sum();
|
findAny 在并行下随便返回一个,比 findFirst 快;如果你不在乎顺序,用 findAny。
八、常见的坑
1. 流只能消费一次
1 2 3
| Stream<String> s = list.stream(); s.count(); s.count();
|
解决办法:每次用都重新 list.stream(),或者先把结果 collect 成集合。
2. 中间操作是惰性的,peek 不触发
1 2 3 4 5 6 7 8
| list.stream() .peek(System.out::println) .filter(x -> x > 0);
list.stream() .peek(System.out::println) .filter(x -> x > 0) .collect(Collectors.toList());
|
3. 自动装箱的性能问题
1 2 3 4 5
| int sum = list.stream().map(x -> x * 2).reduce(0, Integer::sum);
int sum = list.stream().mapToInt(x -> x * 2).sum();
|
处理大量数值时,用 IntStream / LongStream / DoubleStream 避开装箱。
4. 流里改外部变量会编译失败
1 2
| int total = 0; list.stream().forEach(x -> total += x);
|
函数式接口要求捕获的变量不可变。要聚合就走 reduce / collect,别想着在外面改个累加变量。
5. 流里出现 null 元素
1 2 3 4
| List<String> list = Arrays.asList("a", null, "b"); list.stream().map(String::length);
list.stream().filter(Objects::nonNull).map(String::length)
|
源头就在 map 前先 .filter(Objects::nonNull) 把 null 剔掉。
6. toMap 的重复 key
前面写过,重复 key 默认抛异常,记得传合并函数。
7. forEach 顺序和并行
并行流里 forEach 不保证顺序,forEachOrdered 才保序,但会拖慢并行。想要有序输出,不如直接串行。
九、几条实践建议
- 数值处理用原始类型流:int/long/double 走
IntStream 等,少装箱。
- 短路操作往前放:filter 放 map 前面,先把数据量压下来再转换。
- 别在流里搞副作用:不要在
forEach / peek 里改外部状态,结果用 collect / reduce 拿。
sorted 加并行要谨慎:真要排序又数据量大,先想清楚是不是非并行不可。
- 不是所有循环都该换成 Stream:几行 for、需要 break/continue、逻辑绕来绕去的,传统循环反而清楚。Stream 是让数据转换变优雅,不是要把所有 for 消灭。
- 可读性放第一:一条链七八步、flatMap 套 groupingBy 的,拆成中间变量或者分步写,别为了炫全塞一行。
十、一个完整例子
假设有一个 Order 列表,需求:统计每个用户、每个月的订单总金额,并且只保留金额大于 1000 的记录,最后按金额降序输出。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| class Order { String userId; int month; BigDecimal amount; }
List<Order> orders = loadOrders();
Map<String, Map<Integer, BigDecimal>> result = orders.stream() .filter(o -> o.getAmount().compareTo(BigDecimal.valueOf(1000)) > 0) .collect(Collectors.groupingBy(Order::getUserId, Collectors.groupingBy(Order::getMonth, Collectors.reducing( BigDecimal.ZERO, Order::getAmount, BigDecimal::add))));
result.forEach((userId, byMonth) -> { System.out.println("用户 " + userId); byMonth.entrySet().stream() .sorted(Map.Entry.<Integer, BigDecimal>comparingByValue().reversed()) .forEach(e -> System.out.println(" " + e.getKey() + "月: " + e.getValue())); });
|
这个例子把 filter → groupingBy(多级)→ reducing(下游聚合)→ 再 stream 排序输出串了起来,日常大部分聚合需求都能照这个思路写。
十一、收个尾
说到底就三点:
- 数据源 + 中间操作(惰性)+ 终止操作(触发)。
- 无状态操作(filter/map/flatMap)便宜,有状态操作(distinct/sorted/limit)贵,并行时差距更明显。
- 能 collect 就别搞副作用,能用原始流就别装箱。
Stream 不会让你的程序变快,极端情况反而更慢,但它能让数据处理代码更短、更好读、更不容易出 bug。中间操作和 Collectors 那几个方法多用几次就熟了,并行和自定义收集器等真用到了再查也不晚。
参考资料:Java 8 官方 java.util.stream 包文档。