Java8 Stream流 完整学习笔记
📚 一、Stream流概述
什么是Stream流?
Stream是Java8推出的函数式编程特性,可以让你用声明式的方式处理数据集合。就像工厂里的流水线,数据从一个操作流向下一个操作。
核心思想
数据源 → 中间操作1 → 中间操作2 → ... → 终点操作
(集合) (过滤/映射等) (收集结果)
与传统for循环对比
// 传统方式:命令式编程(关注怎么做)
List<UserDTO> dtoList = new ArrayList<>();
for (User user : userList) {
if (user.getAge() > 18) {
UserDTO dto = new UserDTO();
dto.setId(user.getId());
dto.setName(user.getName());
dtoList.add(dto);
}
}
// Stream方式:声明式编程(关注做什么)
List<UserDTO> dtoList = userList.stream()
.filter(user -> user.getAge() > 18)
.map(user -> new UserDTO(user.getId(), user.getName()))
.collect(Collectors.toList());
🎯 二、Stream操作分类
1. 中间操作(Intermediate Operations)
多个中间操作可以链式调用,返回新的Stream流
| 操作 | 作用 | 返回类型 | 示例 |
|---|---|---|---|
filter | 过滤 | Stream | stream.filter(x -> x > 5) |
map | 映射转换 | Stream | stream.map(obj -> obj.getName()) |
flatMap | 扁平化映射 | Stream | stream.flatMap(list -> list.stream()) |
sorted | 排序 | Stream | stream.sorted((a,b) -> a-b) |
distinct | 去重 | Stream | stream.distinct() |
limit | 限制个数 | Stream | stream.limit(10) |
skip | 跳过元素 | Stream | stream.skip(5) |
peek | 查看元素 | Stream | stream.peek(System.out::println) |
2. 终点操作(Terminal Operations)
只能有一个,执行后Stream流就被消耗掉了
| 操作 | 作用 | 返回类型 | 示例 |
|---|---|---|---|
collect | 收集结果 | Collection | stream.collect(Collectors.toList()) |
forEach | 遍历 | void | stream.forEach(System.out::println) |
count | 计数 | long | stream.count() |
anyMatch | 任意匹配 | boolean | stream.anyMatch(x -> x>5) |
allMatch | 全部匹配 | boolean | stream.allMatch(x -> x>0) |
noneMatch | 无一匹配 | boolean | stream.noneMatch(x -> x<0) |
findFirst | 找第一个 | Optional | stream.findFirst() |
findAny | 找任意一个 | Optional | stream.findAny() |
reduce | 归约聚合 | Optional | stream.reduce(0, Integer::sum) |
min/max | 最小/大值 | Optional | stream.min(Comparator.naturalOrder()) |
💡 三、常用操作详解(带实战案例)
准备数据模型
// 实体类:用户
@Data
@AllArgsConstructor
@NoArgsConstructor
class User {
private Long id;
private String username;
private String password;
private Integer age;
private String city;
private List<String> hobbies;
}
// DTO类:用户传输对象
@Data
@AllArgsConstructor
@NoArgsConstructor
class UserDTO {
private Long id;
private String username;
private Integer age;
private String city;
}
// 初始化数据
List<User> userList = Arrays.asList(
new User(1L, "张三", "123456", 25, "北京", Arrays.asList("篮球", "游泳")),
new User(2L, "李四", "123456", 17, "上海", Arrays.asList("足球", "阅读")),
new User(3L, "王五", "123456", 30, "北京", Arrays.asList("跑步", "游泳")),
new User(4L, "赵六", "123456", 17, "广州", Arrays.asList("游戏", "音乐")),
new User(5L, "田七", "123456", 22, "上海", Arrays.asList("篮球", "旅行"))
);
案例1:基础转换(map + collect)
// 场景:将User转换为UserDTO(隐藏敏感信息)
List<UserDTO> dtoList = userList.stream()
.map(user -> {
UserDTO dto = new UserDTO();
dto.setId(user.getId());
dto.setUsername(user.getUsername());
dto.setAge(user.getAge());
dto.setCity(user.getCity());
return dto;
})
.collect(Collectors.toList());
// 简化版:使用构造方法
List<UserDTO> dtoList = userList.stream()
.map(user -> new UserDTO(
user.getId(),
user.getUsername(),
user.getAge(),
user.getCity()
))
.collect(Collectors.toList());
// 终极简化:方法引用(需要在UserDTO中定义构造方法)
List<UserDTO> dtoList = userList.stream()
.map(UserDTO::new)
.collect(Collectors.toList());
案例2:过滤 + 转换(filter + map + collect)
// 场景:只转换成年用户(年龄≥18)
List<UserDTO> adultUsers = userList.stream()
.filter(user -> user.getAge() >= 18) // 过滤:只要成年人
.filter(user -> "北京".equals(user.getCity())) // 再加个城市过滤
.map(user -> new UserDTO(
user.getId(),
user.getUsername(),
user.getAge(),
user.getCity()
))
.collect(Collectors.toList());
System.out.println("北京成年用户: " + adultUsers);
案例3:排序 + 去重 + 限制(sorted + distinct + limit)
// 场景:获取年龄最大的3个不同城市的用户
List<UserDTO> top3Users = userList.stream()
.sorted((u1, u2) -> u2.getAge() - u1.getAge()) // 按年龄降序
.map(user -> new UserDTO(
user.getId(),
user.getUsername(),
user.getAge(),
user.getCity()
))
.distinct() // 根据equals方法去重(需要重写DTO的equals)
.limit(3) // 只取前3个
.collect(Collectors.toList());
案例4:分组统计(groupingBy)
// 场景:按城市分组
Map<String, List<User>> groupByCity = userList.stream()
.collect(Collectors.groupingBy(User::getCity));
// 场景:按城市分组,并统计每个城市的人数
Map<String, Long> cityCount = userList.stream()
.collect(Collectors.groupingBy(
User::getCity,
Collectors.counting()
));
// 场景:按城市分组,并计算平均年龄
Map<String, Double> cityAvgAge = userList.stream()
.collect(Collectors.groupingBy(
User::getCity,
Collectors.averagingInt(User::getAge)
));
案例5:复杂映射(flatMap)
// 场景:提取所有用户的兴趣爱好,去重后排序
List<String> allHobbies = userList.stream()
.map(User::getHobbies) // Stream<List<String>>
.flatMap(List::stream) // Stream<String> 扁平化
.distinct() // 去重
.sorted() // 排序
.collect(Collectors.toList());
System.out.println("所有兴趣爱好: " + allHobbies);
// 输出:[篮球, 跑步, 游泳, 游戏, 足球, 音乐, 阅读, 旅行]
案例6:判断与查找(anyMatch/findFirst)
// 场景1:判断是否存在上海用户
boolean hasShanghai = userList.stream()
.anyMatch(user -> "上海".equals(user.getCity()));
// 场景2:查找第一个北京用户
Optional<User> firstBeijingUser = userList.stream()
.filter(user -> "北京".equals(user.getCity()))
.findFirst();
firstBeijingUser.ifPresent(user ->
System.out.println("第一个北京用户: " + user.getUsername())
);
// 场景3:查找任意一个未成年用户
Optional<User> anyMinor = userList.stream()
.filter(user -> user.getAge() < 18)
.findAny(); // 在并行流中更高效
案例7:统计聚合(count/sum/average)
// 计数
long adultCount = userList.stream()
.filter(user -> user.getAge() >= 18)
.count();
// 年龄总和
int totalAge = userList.stream()
.mapToInt(User::getAge)
.sum();
// 平均年龄
double avgAge = userList.stream()
.mapToInt(User::getAge)
.average()
.orElse(0);
// 最大年龄
int maxAge = userList.stream()
.mapToInt(User::getAge)
.max()
.orElse(0);
// 一次性获取多个统计值
IntSummaryStatistics stats = userList.stream()
.mapToInt(User::getAge)
.summaryStatistics();
System.out.println("总数: " + stats.getCount());
System.out.println("平均: " + stats.getAverage());
System.out.println("最大: " + stats.getMax());
System.out.println("最小: " + stats.getMin());
案例8:reduce归约
// 场景:拼接所有用户名
String allNames = userList.stream()
.map(User::getUsername)
.reduce("", (a, b) -> a + ", " + b);
// 或者用更简单的:collect(Collectors.joining(", "))
// 场景:计算年龄总和(用reduce)
Integer sumAge = userList.stream()
.map(User::getAge)
.reduce(0, Integer::sum);
🛠️ 四、Collectors工具类详解
// 1. 转List
List<User> list = stream.collect(Collectors.toList());
// 2. 转Set
Set<User> set = stream.collect(Collectors.toSet());
// 3. 转Map
Map<Long, String> idToNameMap = userList.stream()
.collect(Collectors.toMap(
User::getId, // key
User::getUsername, // value
(v1, v2) -> v1 // key冲突时的处理
));
// 4. 分组
Map<String, List<User>> groupByCity = userList.stream()
.collect(Collectors.groupingBy(User::getCity));
// 5. 分区(根据boolean条件)
Map<Boolean, List<User>> partitionByAdult = userList.stream()
.collect(Collectors.partitioningBy(user -> user.getAge() >= 18));
// 6. 连接字符串
String names = userList.stream()
.map(User::getUsername)
.collect(Collectors.joining(", ", "[", "]")); // 前缀、分隔符、后缀
// 7. 统计
Double avgAge = userList.stream()
.collect(Collectors.averagingInt(User::getAge));
// 8. 嵌套收集
Map<String, List<String>> cityToNames = userList.stream()
.collect(Collectors.groupingBy(
User::getCity,
Collectors.mapping(User::getUsername, Collectors.toList())
));
🚀 五、实战:完整业务场景
场景:用户管理系统
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
/**
* 获取成年用户DTO列表(按年龄排序,取前10个)
*/
public List<UserDTO> getAdultUsersTop10() {
return userRepository.findAll().stream()
.filter(user -> user.getAge() >= 18) // 过滤
.sorted(Comparator.comparing(User::getAge)) // 排序
.limit(10) // 限制数量
.map(this::convertToDTO) // 转换
.collect(Collectors.toList()); // 收集
}
/**
* 按城市分组统计用户信息
*/
public Map<String, CityUserStats> getUserStatsByCity() {
return userRepository.findAll().stream()
.collect(Collectors.groupingBy(
User::getCity,
Collectors.collectingAndThen(
Collectors.toList(),
this::calculateCityStats
)
));
}
/**
* 计算城市统计信息
*/
private CityUserStats calculateCityStats(List<User> users) {
CityUserStats stats = new CityUserStats();
stats.setTotalCount(users.size());
stats.setAverageAge(users.stream()
.mapToInt(User::getAge)
.average()
.orElse(0));
stats.setUserNames(users.stream()
.map(User::getUsername)
.collect(Collectors.toList()));
return stats;
}
/**
* User转UserDTO
*/
private UserDTO convertToDTO(User user) {
UserDTO dto = new UserDTO();
BeanUtils.copyProperties(user, dto);
return dto;
}
}
// 城市统计DTO
@Data
class CityUserStats {
private Integer totalCount;
private Double averageAge;
private List<String> userNames;
}
📝 六、注意事项与最佳实践
1. 惰性求值
// Stream是惰性求值的,没有终点操作就不会执行
userList.stream()
.filter(user -> {
System.out.println("过滤: " + user.getUsername());
return user.getAge() > 18;
}); // 没有collect,不会打印任何东西
// 加上终点操作才会执行
userList.stream()
.filter(user -> {
System.out.println("过滤: " + user.getUsername());
return user.getAge() > 18;
})
.collect(Collectors.toList()); // 会打印
2. Stream不能重复使用
Stream<User> stream = userList.stream();
stream.collect(Collectors.toList()); // 第一次使用
stream.collect(Collectors.toList()); // 报错!stream has already been operated upon
3. 并行流的使用
// 大数据量时使用并行流提高性能
List<UserDTO> dtoList = userList.parallelStream() // 或 .stream().parallel()
.filter(user -> user.getAge() > 18)
.map(this::convertToDTO)
.collect(Collectors.toList());
// 注意:并行流不保证顺序,且线程不安全的数据要小心
4. 空指针处理
// 集合本身为null时要处理
List<User> safeList = userList == null ? new ArrayList<>() : userList;
List<UserDTO> dtoList = safeList.stream()
.filter(Objects::nonNull) // 过滤掉null元素
.map(this::convertToDTO)
.collect(Collectors.toList());
5. 性能考虑
- 小数据量:传统for循环可能更快
- 大数据量:Stream(特别是并行流)优势明显
- 复杂操作:Stream代码更简洁、易读
🎯 七、常见面试题
-
Stream和for循环哪个性能好?
- 数据量小:for循环
- 数据量大:并行流
- 复杂操作:Stream更简洁
-
map和flatMap的区别?
- map:一对一转换
- flatMap:一对多转换,扁平化处理
-
中间操作和终点操作的区别?
- 中间操作返回Stream,可以链式调用
- 终点操作返回结果,Stream被消费
-
如何自定义Collector?
- 实现Collector接口
- 或使用Collectors.of()静态方法
📖 八、总结
Stream流的优势
✅ 代码简洁:一行顶以前十行
✅ 可读性强:链式调用,逻辑清晰
✅ 易于并行:parallelStream一键并行
✅ 函数式编程:关注做什么,而不是怎么做
适用场景
- 集合的批量操作
- 复杂的数据转换
- 多级过滤和分组统计
- 大数据量的并行处理
不适用场景
- 简单的for循环遍历
- 需要break/return的复杂逻辑
- 性能要求极高的简单操作