收集器结果适配
在流处理过程中,标准收集器通常能独立完成数据收集工作。但存在一类特殊收集器,允许在收集前后对结果进行修改,这类收集器通过Collectors
类的静态方法实现。
collectingAndThen方法
该方法在收集完成后通过finisher函数修改结果类型,其签名如下:
Collector collectingAndThen(
Collector downstream,
Function finisher)
典型应用场景包括生成不可修改集合视图。例如创建人员名称的不可修改列表:
List names = Person.persons()
.stream()
.map(Person::getName)
.collect(Collectors.collectingAndThen(
Collectors.toList(),
Collections::unmodifiableList));
日历案例演示了更复杂的应用:
- 处理缺失月份
- 按月份排序
- 生成不可修改Map
Map dobCalendar = Person.persons()
.stream()
.collect(collectingAndThen(
groupingBy(p -> p.getDob().getMonth(),
mapping(Person::getName, joining(", "))),
result -> {
// 补全缺失月份
Arrays.stream(Month.values())
.forEach(m -> result.putIfAbsent(m, "None"));
// 返回排序后的不可修改Map
return Collections.unmodifiableMap(
new TreeMap<>(result));
}));
filtering方法(Java 9+)
该方法在分组后收集前进行元素过滤,其签名如下:
Collector filtering(
Predicate predicate,
Collector downstream)
示例:按性别分组并筛选收入超过8000的人员
Map> makingOver8000 =
Person.persons()
.stream()
.collect(groupingBy(
Person::getGender,
filtering(p -> p.getIncome() > 8000.00, toList())));
注意与filter()
操作的区别:filtering()
会保留空分组,而filter()
会完全移除不满足条件的组。
flatMapping方法(Java 9+)
该方法在分组时应用扁平化映射,特别适合处理嵌套集合,其签名如下:
Collector flatMapping(
Function> mapper,
Collector downstream)
示例:统计各性别掌握的语言(去重)
Map> langByGender =
list.stream()
.collect(groupingBy(
Entry::getKey,
flatMapping(e -> e.getValue().stream(), toSet())));
操作组合策略
- 类型转换:通过finisher函数改变最终结果类型
- 后处理:在收集完成后进行结果修饰
- 条件过滤:在分组中间阶段进行元素筛选
- 嵌套处理:对分组元素进行扁平化操作
这些方法特别适合处理需要多阶段转换的复杂收集场景,在保持流式操作链式调用的同时,提供了更灵活的结果控制能力。
流操作基础
映射操作(map)
映射操作通过map()
方法实现一对一元素转换,支持原始类型特化方法(如mapToInt
、mapToDouble
等)。该操作不改变流中元素数量,仅转换元素值:
// 整数平方映射
IntStream.rangeClosed(1, 5)
.map(n -> n * n)
.forEach(System.out::println);
// 对象属性映射
Person.persons()
.stream()
.map(Person::getName)
.forEach(System.out::println);
原始类型流提供更高效的映射方法:
DoubleStream incomeStream = Person.persons()
.stream()
.mapToDouble(Person::getIncome);
过滤操作(filter)
基于谓词筛选元素,返回相同类型的新流。包含多个变体方法:
// 基础过滤
Person.persons()
.stream()
.filter(Person::isFemale)
.forEach(System.out::println);
// 组合过滤条件
Person.persons()
.stream()
.filter(p -> p.isMale() && p.getIncome() > 5000.0)
.forEach(System.out::println);
特殊过滤方法:
skip(n)
:跳过前n个元素limit(max)
:限制最大元素数dropWhile(predicate)
(Java 9+):丢弃满足条件的起始元素takeWhile(predicate)
(Java 9+):保留满足条件的起始元素
// dropWhile示例
Stream.of(1, 2, 3, 4, 5, 6, 7)
.dropWhile(e -> e < 5)
.forEach(System.out::println); // 输出5,6,7
调试操作(peek)
用于观察流管道中的元素处理过程:
Stream.of(1, 2, 3, 4, 5)
.peek(e -> System.out.println("原始值: " + e))
.filter(n -> n % 2 == 1)
.peek(e -> System.out.println("过滤后: " + e))
.map(n -> n * n)
.forEach(System.out::println);
遍历操作(forEach)
提供有序和无序两种处理方式:
// 基础遍历
Person.persons()
.stream()
.filter(Person::isFemale)
.forEach(System.out::println);
// 并行流中的有序遍历(性能较低)
Person.persons()
.parallelStream()
.forEachOrdered(System.out::println);
注意在并行流中:
forEach
不保证处理顺序forEachOrdered
保证顺序但影响性能- 修改外部状态需注意线程安全问题
操作组合策略
-
链式调用:保持操作管道连贯性
persons.stream() .filter(p -> p.getAge() > 30) .map(Person::getName) .sorted() .forEach(System.out::println);
-
原始类型优化:优先使用
mapToXxx
方法避免装箱开销 -
短路操作:合理使用
limit
/takeWhile
提前终止处理 -
调试技巧:在关键操作前后插入
peek()
观察数据变化
这些基础操作构成了流式处理的核心模式,通过组合不同操作可以实现复杂的数据处理流水线。
扁平化与复杂映射
flatMap核心原理
flatMap
操作实现一对多元素转换,通过三个步骤完成流扁平化处理:
- 原始映射:将每个输入元素转换为新流
- 中间结构:生成
Stream>
嵌套结构 - 最终扁平化:将所有子流合并为单一流
// 错误示范:产生Stream>
Stream.of(1, 2, 3)
.map(n -> Stream.of(n, n * n))
.forEach(System.out::println); // 输出流对象引用
// 正确实现
Stream.of(1, 2, 3)
.flatMap(n -> Stream.of(n, n * n))
.forEach(System.out::println); // 输出1,1,2,4,3,9
多语言统计案例
处理嵌套集合时,flatMapping
收集器可高效展开分组元素。以下示例统计各性别掌握的不同语言:
List>> people = List.of(
entry("Male", Set.of("English", "French")),
entry("Female", Set.of("Spanish", "Wu")));
Map> result = people.stream()
.collect(groupingBy(
Entry::getKey,
flatMapping(e -> e.getValue().stream(), toSet())));
// 输出:{Female=[Spanish, Wu], Male=[English, French]}
关键实现要点:
- 使用
Entry
结构保存原始数据 groupingBy
按性别分组flatMapping
展开语言集合流toSet
自动去重
字符级流处理
字符串处理时需要特别注意类型转换链:
long count = Stream.of("Java", "Stream")
.flatMap(s -> s.chars().mapToObj(c -> (char)c))
.filter(c -> c == 'a' || c == 'A')
.count(); // 输出3
替代方案(避免中间装箱):
long count = Stream.of("Java", "Stream")
.flatMapToInt(String::chars)
.filter(c -> c == 'a' || c == 'A')
.count();
原始类型优化
针对不同场景的扁平化方案选择:
场景 | 推荐方法 | 优势 |
---|---|---|
对象转对象流 | flatMap | 类型安全 |
对象转原始流 | flatMapToInt/Long/Double | 避免装箱 |
原始转对象流 | mapToObj +flatMap | 灵活转换 |
原始转原始流 | 特化方法如flatMapToInt | 最佳性能 |
示例(统计文本行中所有数字):
List lines = List.of("1a2", "3b4");
IntStream numbers = lines.stream()
.flatMapToInt(line ->
line.chars()
.filter(Character::isDigit)
.map(Character::getNumericValue));
归约操作与聚合计算
reduce方法三重重载
归约操作通过reduce()
方法实现元素聚合计算,提供三种核心变体:
- 基础版本:包含初始值和累加器
T reduce(T identity, BinaryOperator accumulator)
典型应用如数值求和:
List numbers = List.of(1, 2, 3);
int sum = numbers.stream().reduce(0, Integer::sum);
- 无初始值版本:返回Optional处理空流情况
Optional reduce(BinaryOperator accumulator)
适用于极值计算:
Optional max = Stream.of(1, 5, 3)
.reduce(Integer::max);
- 并行优化版本:增加合并器参数
U reduce(U identity,
BiFunction accumulator,
BinaryOperator combiner)
支持并行流的分段计算与结果合并:
double sum = Person.persons()
.parallelStream()
.reduce(0.0,
(partial, p) -> partial + p.getIncome(),
Double::sum);
执行过程对比
顺序流与并行流的处理差异显著:
顺序流执行轨迹:
List.of(1, 2, 3).stream()
.reduce(0, (a,b) -> {
System.out.printf("%d + %d%n", a, b);
return a + b;
});
// 输出:
// 0 + 1
// 1 + 2
// 3 + 3
并行流执行轨迹:
List.of(1, 2, 3).parallelStream()
.reduce(0, (a,b) -> {
System.out.printf("%s: %d + %d%n",
Thread.currentThread().getName(), a, b);
return a + b;
}, (a,b) -> {
System.out.printf("合并: %d + %d%n", a, b);
return a + b;
});
// 可能输出:
// Thread-1: 0 + 2
// Thread-2: 0 + 3
// main: 0 + 1
// 合并: 1 + 2
// 合并: 3 + 3
原始类型特化方法
针对数值流优化的快捷操作:
方法 | 返回类型 | 说明 |
---|---|---|
sum() | 原始类型 | 元素求和 |
max() | OptionalXxx | 最大值(空流返回空) |
min() | OptionalXxx | 最小值(空流返回空) |
average() | OptionalDouble | 算术平均值 |
应用示例:
DoubleSummaryStatistics stats = Person.persons()
.stream()
.mapToDouble(Person::getIncome)
.summaryStatistics();
// 可获取count/sum/min/max/average
计数实现方案
三种等效的计数实现方式:
- 直接计数:
long count = stream.count();
- map-reduce模式:
long count = stream.mapToLong(e -> 1L).sum();
- 纯reduce实现:
long count = stream.reduce(0L,
(total, e) -> total + 1L,
Long::sum);
性能优化建议
- 优先选择原始类型流:避免自动装箱开销
// 优于Stream
IntStream.range(1,100)
- 合理使用并行:数据量大时提升吞吐量
largeList.parallelStream()
.mapToInt(...)
.sum();
- 短路操作优化:及早终止不必要的计算
stream.filter(...)
.findFirst()
.orElse(defaultValue);
归约操作是流式编程的核心计算范式,理解其底层机制对于构建高效数据处理管道至关重要。通过合理选择操作变体和优化策略,可以实现从简单聚合到复杂计算的各类需求。
流操作类型与特性
操作分类体系
流操作分为两大核心类型:
- 中间操作(Intermediate Operations)
- 无状态操作:
filter
、map
、flatMap
等 - 有状态操作:
distinct
、sorted
、limit
等
- 终端操作(Terminal Operations)
- 短路操作:
anyMatch
、findFirst
等 - 非短路操作:
forEach
、reduce
、collect
等
// 操作链示例
List names = persons.stream() // 获取流
.filter(p -> p.getAge() > 18) // 无状态中间操作
.distinct() // 有状态中间操作
.map(Person::getName) // 无状态中间操作
.limit(10) // 有状态中间操作
.collect(Collectors.toList()); // 终端操作
原始类型流优化
针对数值计算场景,原始类型流(Int/Long/DoubleStream)可显著提升性能:
// 对象流 vs 原始类型流性能对比
long start = System.nanoTime();
double sum = persons.stream() // Stream
.mapToDouble(Person::getIncome) // DoubleStream
.sum();
long duration = System.nanoTime() - start;
优化建议:
- 优先使用
mapToInt
/mapToDouble
等特化方法 - 终端操作选择原始流专用方法(如
sum()
、average()
) - 避免在原始流与对象流间频繁转换
并行流注意事项
并行处理需平衡性能与正确性:
// 安全使用并行reduce
double parallelSum = persons.parallelStream()
.reduce(0.0,
(partialSum, p) -> partialSum + p.getIncome(), // 累加器
Double::sum); // 合并器
// 线程安全收集
ConcurrentMap> byDept = employees.parallelStream()
.collect(Collectors.groupingByConcurrent(Employee::getDepartment));
关键约束:
- 非线程安全集合需使用并发收集器
- 有状态lambda表达式会导致数据竞争
- 有序操作(如
limit
)会降低并行性能
控制操作增强
Java 9引入的流控制方法:
// takeWhile示例(获取有序流中满足条件的起始元素)
List result = Stream.of(1, 2, 3, 4, 5)
.takeWhile(n -> n < 4)
.collect(Collectors.toList()); // [1, 2, 3]
// dropWhile示例(丢弃有序流中满足条件的起始元素)
Stream.of(1, 2, 3, 4, 5)
.dropWhile(n -> n < 3)
.forEach(System.out::println); // 输出3,4,5
注意事项:
- 在无序流上行为不确定
- 与
filter
的区别在于只判断起始连续元素 - 适合处理已排序流的截断需求
调试技巧
使用peek
监控流处理过程:
List collected = Stream.of("a", "b", "c")
.peek(e -> System.out.println("原始值: " + e))
.map(String::toUpperCase)
.peek(e -> System.out.println("映射后: " + e))
.collect(Collectors.toList());
调试要点:
- 避免在
peek
中修改流元素 - 并行流中输出可能交错
- 生产环境应移除调试
peek
调用
这些核心操作特性构成了流式处理的基础能力,开发者需要根据具体场景选择适当的操作组合,平衡性能、可读性和功能需求。