文章目录
一、Stream 流简介
Java 8 引入的 Stream
提供了一种高效、声明式处理集合数据的方式,支持顺序和并行操作,核心特点包括:
- 链式调用:通过组合中间操作(如
filter
,map
)和终端操作(如collect
,forEach
)实现复杂逻辑。 - 延迟执行:只有终端操作触发时才会执行中间操作。
- 不可重用:每个流只能被消费一次。
二、Stream 流核心操作
1. 创建 Stream
集合创建:
Collection.stream()
或parallelStream()
。List<String> list = Arrays.asList("a", "b", "c"); Stream<String> stream = list.stream();
数组创建:
Arrays.stream(array)
。String[] arr = {"a", "b", "c"}; Stream<String> stream = Arrays.stream(arr);
静态方法:
Stream.of()
或生成无限流Stream.iterate()
,Stream.generate()
。Stream<Integer> numbers = Stream.of(1, 2, 3); Stream<Integer> infiniteStream = Stream.iterate(0, n -> n + 2); // 0, 2, 4, ...
2. 中间操作(Intermediate Operations)
filter(Predicate):过滤数据
作用:根据条件筛选元素,保留满足条件的元素。
示例:
List<String> filtered = list.stream()
.filter(s -> s.startsWith("a")) // 保留以"a"开头的字符串
.collect(Collectors.toList());
最佳实践:
1. 简单条件过滤
筛选出符合条件的元素。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
// 筛选所有偶数
List<Integer> evenNumbers = numbers.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList()); // [2, 4, 6]
2. 多条件组合
使用逻辑运算符 &&
(与)、||
(或)组合条件。
List<String> words = Arrays.asList("apple", "banana", "cherry", "date");
// 筛选长度大于5 且 以字母a开头的单词
List<String> result = words.stream()
.filter(s -> s.length() > 5 && s.startsWith("b"))
.collect(Collectors.toList()); // ["banana"]
3. 过滤对象集合
根据对象属性筛选数据。
class User {
String name;
int age;
// 构造方法、getter/setter 省略
}
List<User> users = Arrays.asList(
new User("Alice", 25),
new User("Bob", 17),
new User("Charlie", 30)
);
// 筛选年龄 >= 18 的用户
List<User> adults = users.stream()
.filter(user -> user.getAge() >= 18)
.collect(Collectors.toList()); // [Alice, Charlie]
4. 过滤 null
值
使用 Objects::nonNull
过滤掉 null
元素。
List<String> listWithNulls = Arrays.asList("a", null, "b", null, "c");
// 过滤掉所有 null 值
List<String> nonNullList = listWithNulls.stream()
.filter(Objects::nonNull)
.collect(Collectors.toList()); // ["a", "b", "c"]
2. map(Function<T, R>):转换元素
map
是 Java Stream 中最核心的中间操作之一,用于将流中的元素一对一转换为另一种形式。它的本质是通过一个函数(Function<T, R>
)对每个元素进行映射,生成新的元素流。以下是 map
的深入解析,涵盖使用场景、底层机制、最佳实践与常见问题。
一、map
的核心特性
- 一对一转换:每个输入元素对应一个输出元素,元素数量不变。
- 类型转换:输入类型
T
可转换为任意输出类型R
(如String
→Integer
)。 - 惰性求值:只有终端操作触发时才会执行映射逻辑。
- 无副作用:理想情况下,映射函数不修改外部状态(符合函数式编程原则)。
作用:将元素转换为另一种类型或提取特定属性。
1. 简单类型转换
// 将字符串转换为大写
List<String> upperCaseList = Arrays.asList("apple", "banana", "cherry").stream()
.map(String::toUpperCase)
.collect(Collectors.toList()); // ["APPLE", "BANANA", "CHERRY"]
2. 提取对象属性
// 从User对象中提取name属性
List<String> names = users.stream()
.map(User::getName)
.collect(Collectors.toList());
3. 复杂转换逻辑
// 将字符串转换为自定义DTO对象
List<DataDTO> dtos = strings.stream()
.map(s -> {
DataDTO dto = new DataDTO();
dto.setValue(s.length());
dto.setLabel(s.toUpperCase());
return dto;
})
.collect(Collectors.toList());
3. flatMap(Function<T, Stream>):扁平化嵌套结构
作用:将嵌套集合(如 List<List<T>>
)展开为单一流。
示例:
List<List<String>> nestedList = Arrays.asList(
Arrays.asList("a", "b"),
Arrays.asList("c", "d")
);
List<String> flatList = nestedList.stream()
.flatMap(Collection::stream) // 将每个List<String>转换为Stream<String>
.collect(Collectors.toList()); // ["a", "b", "c", "d"]
典型场景:
- 处理数据库查询的多表关联结果。
- 合并多个API响应的数据列表。
4. distinct():去重
作用:基于 equals()
和 hashCode()
去重。
示例:
List<Integer> unique = numbers.stream()
.distinct()
.collect(Collectors.toList()); // [1, 2, 3]
关键点:
自定义对象去重:需重写
equals()
和hashCode()
。class User { private Long id; @Override public boolean equals(Object o) { /* 基于id比较 */ } @Override public int hashCode() { /* 基于id生成 */ } }
性能注意:对大数据集去重可能消耗内存,可结合
limit
分批次处理。
5. sorted():排序
作用:按自然顺序或自定义比较器排序。
示例:
List<String> sorted = list.stream()
.sorted(Comparator.reverseOrder()) // 逆序排序
.collect(Collectors.toList());
优化建议:
- 尽早过滤:先
filter
减少待排序数据量。 - 避免频繁排序:对需要多次排序的场景,考虑转换为有序集合(如
TreeSet
)。
6. limit(n) 和 skip(n):分页控制
作用:skip
跳过前N个元素,limit
限制返回数量。
示例:
List<Integer> result = Stream.iterate(0, n -> n + 1)
.skip(5) // 跳过0-4,从5开始
.limit(10) // 取5-14
.collect(Collectors.toList());
应用场景:
分页查询:模拟数据库分页。
int page = 2, size = 10; List<User> users = allUsers.stream() .skip((page - 1) * size) .limit(size) .collect(Collectors.toList());
性能注意:对非顺序流(如并行流),
skip
和limit
可能无法保证预期结果。3. 终端操作(Terminal Operations)
遍历元素:
forEach(Consumer<T>)
list.stream().forEach(System.out::println);
收集结果:
collect(Collector)
List<String> list = stream.collect(Collectors.toList()); Set<String> set = stream.collect(Collectors.toSet()); String joined = stream.collect(Collectors.joining(", "));
统计数量:
count()
long count = list.stream().filter(s -> s.length() > 3).count();
匹配检查:
anyMatch(Predicate<T>)
:至少一个元素匹配。allMatch(Predicate<T>)
:所有元素匹配。noneMatch(Predicate<T>)
:没有元素匹配。
boolean hasA = list.stream().anyMatch(s -> s.contains("a"));
查找元素:
findFirst()
:返回第一个元素(Optional<T>
)。findAny()
:适用于并行流,返回任意元素。
Optional<String> first = list.stream().findFirst();
归约操作:
reduce(BinaryOperator<T>)
Optional<Integer> sum = Stream.of(1, 2, 3).reduce(Integer::sum); // 6
三、高级用法
1. 分组与分区
分组:
Collectors.groupingBy()
Map<Integer, List<String>> groupByLength = list.stream() .collect(Collectors.groupingBy(String::length)); // 按字符串长度分组
分区:
Collectors.partitioningBy()
Map<Boolean, List<String>> partition = list.stream() .collect(Collectors.partitioningBy(s -> s.length() > 3)); // 按条件分为两组
2. 并行流处理
创建并行流:
.parallel()
或parallelStream()
。List<String> result = list.parallelStream() .filter(s -> s.length() > 3) .collect(Collectors.toList());
注意事项:
- 确保操作线程安全(如避免修改共享变量)。
- 并行流可能不适用于小数据量或复杂中间操作。
3. 原始类型流
避免装箱开销:使用
IntStream
,LongStream
,DoubleStream
。IntStream.range(1, 5).forEach(System.out::println); // 1, 2, 3, 4 LongStream.of(10L, 20L).sum();
四、实际应用场景示例
1. 数据转换与过滤
// 从用户列表中提取成年用户的姓名
List<String> adultNames = users.stream()
.filter(user -> user.getAge() >= 18)
.map(User::getName)
.collect(Collectors.toList());
2. 统计与汇总
// 计算订单总金额
double totalAmount = orders.stream()
.mapToDouble(Order::getAmount)
.sum();
3. 复杂集合处理
// 将多个订单的商品列表合并并去重
Set<String> allProducts = orders.stream()
.flatMap(order -> order.getProducts().stream())
.collect(Collectors.toSet());
4. 分组统计
// 按部门分组统计员工平均工资
Map<String, Double> avgSalaryByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.averagingDouble(Employee::getSalary)
);
五、注意事项与最佳实践
- 避免副作用:在流操作中不要修改外部变量,尤其是在并行流中。
- 优先使用无状态操作:如
filter
,map
比sorted
,distinct
更高效。 - 谨慎使用并行流:仅在数据量大且操作耗时的情况下考虑并行化。
- 减少装箱开销:对数值操作使用原始类型流(
IntStream
等)。 - 合理使用短路操作:如
findFirst()
,limit()
可提前终止流处理。
六、与传统循环的对比
场景 | 传统循环 | Stream 流 |
---|---|---|
简单遍历 | 直接易读 | 代码更简洁,但可能略微性能开销 |
复杂数据处理 | 需多层嵌套循环,代码冗长 | 链式调用,逻辑清晰 |
并行处理 | 需手动管理线程和同步 | 通过 .parallel() 自动并行化 |
函数式编程支持 | 需额外工具类配合 | 原生支持 Lambda 和方法引用 |
通过掌握 Stream 流的常见用法,可以显著提升代码的可读性和开发效率,尤其在处理集合数据时,能够以更简洁的方式实现复杂的数据操作。