【Stream API学习】
Stream API学习
- 一、 什么是 Stream?
- 二、Stream 操作的三个步骤
- 三、详细操作说明
- 1. 创建 Stream
- 2. 中间操作(Intermediate Operations)
- 3. 终端操作(Terminal Operations)
- 四、核心代码示例
- 示例1:找出低卡路里(<400)的菜品名称,并按卡路里排序
- 示例2:统计菜单中有多少种肉菜
- 示例3:使用 flatMap 处理嵌套集合
- 示例4:使用 reduce 求和
- 五、并行流(Parallel Stream)
- 六、总结与最佳实践
一、 什么是 Stream?
你可以把 Stream 想象成一个高级的迭代器(Iterator),但它更强大。它代表的是一个数据序列,这个序列支持顺序和并行的聚合操作。
核心思想:不是直接操作数据,而是描述对数据做什么。例如,“找出所有大于10的数,然后排序,最后打印出来”。这种风格叫声明式编程(告诉计算机“做什么”),区别于传统的命令式编程(告诉计算机“怎么做”)。
主要特点:
-
不是数据结构:它自己不存储数据,数据来自数据源(如集合、数组、I/O通道)。
-
函数式风格:对流的操作会产生一个结果,但不会修改其底层的数据源。
-
惰性执行:很多中间操作(如 filter, map)是惰性的,只有遇到终端操作时,整个操作链才会被执行。
-
可消费性:每个 Stream 只能被“消费”一次。一旦调用了终端操作,流就关闭了,不能再使用。
二、Stream 操作的三个步骤
使用 Stream 通常包含三个步骤:
-
创建 Stream:从一个数据源(如集合)获取一个流。
-
中间操作:对数据进行处理/转换(如过滤、映射、排序),返回一个新的 Stream,可以连续操作。
-
终端操作:执行操作链,并产生结果。执行后,该流就不能再被使用。
流程:数据源 -> 创建流 -> 中间操作1 -> 中间操作2 -> … -> 终端操作 -> 结果
三、详细操作说明
1. 创建 Stream
方法 | 描述 | 示例 |
---|---|---|
collection.stream() | 最常用的方式,从集合创建顺序流 | list.stream() |
collection.parallelStream() | 创建并行流,可利用多核优势 | list.parallelStream() |
Arrays.stream(T[] array) | 从数组创建流 | Arrays.stream(new int[]{1,2,3}) |
Stream.of(T… values) | 直接传入一系列值创建流 | Stream.of(“a”, “b”, “c”) |
Stream.iterate() | 生成无限流(需用 limit 限制) | Stream.iterate(0, n -> n+2) |
Stream.generate() | 通过 Supplier 生成无限流 | Stream.generate(Math::random) |
2. 中间操作(Intermediate Operations)
重要:中间操作是惰性的,不遇到终端操作不会执行。
操作 | 描述 | 示例 |
---|---|---|
filter(Predicate) | 过滤,保留满足条件的元素 | .filter(s -> s.length() > 3) |
map(Function<T, R>) | 映射,将元素转换成其他形式或提取信息 | .map(String::toUpperCase) |
flatMap(Function<T, Stream>) | 扁平化映射,将每个元素转换成一个流,然后把所有流连接起来 | .flatMap(list -> list.stream()) |
distinct() | 去重,根据 equals() 方法去重 | .distinct() |
sorted() / sorted(Comparator) | 排序 | .sorted() / .sorted(Comparator.reverseOrder()) |
limit(long maxSize) | 限制,截断流,使其元素不超过给定数量 | .limit(10) |
skip(long n) | 跳过,跳过前 n 个元素 | .skip(5) |
peek(Consumer) | “窥视”,对每个元素执行操作,主要用于调试 | .peek(System.out::println) |
3. 终端操作(Terminal Operations)
执行后,流管道被消费,无法再使用。
操作 | 描述 | 示例 | 返回类型 |
---|---|---|---|
forEach(Consumer) | 遍历,对每个元素执行操作 | .forEach(System.out::println) | void |
collect(Collector) | 收集,将流转换为其他形式(如 List, Set, Map) | .collect(Collectors.toList()) | 集合等 |
toArray() | 转为数组 | .toArray() | Object[] |
reduce(…) | 归约,将流中元素反复结合,得到一个值 | .reduce(0, Integer::sum) | Optional 或 T |
min(Comparator) / max(Comparator) | 求最小值/最大值 | .min(Comparator.naturalOrder()) | Optional |
count() | 计数,返回流中元素个数 | .count() | long |
anyMatch(Predicate) | 任意匹配,检查是否至少有一个元素匹配 | .anyMatch(s -> s.startsWith(“A”)) | boolean |
allMatch(Predicate) | 全部匹配 | .allMatch(s -> s.length() > 0) | boolean |
noneMatch(Predicate) | 没有匹配 | .noneMatch(s -> s.isEmpty()) | boolean |
findFirst() | 返回第一个元素 | .findFirst() | Optional |
findAny() | 返回任意一个元素(在并行流中效率更高) | .findAny() | Optional |
四、核心代码示例
让我们通过一些例子来加深理解。
假设有一个 Dish 类和一个菜单列表:
public class Dish {private final String name;private final boolean vegetarian;private final int calories;private final Type type;// 构造器、getter、toString 省略public enum Type { MEAT, FISH, OTHER }
}List<Dish> menu = Arrays.asList(new Dish("pork", false, 800, Dish.Type.MEAT),new Dish("beef", false, 700, Dish.Type.MEAT),new Dish("chicken", false, 400, Dish.Type.MEAT),new Dish("french fries", true, 530, Dish.Type.OTHER),new Dish("rice", true, 350, Dish.Type.OTHER),new Dish("season fruit", true, 120, Dish.Type.OTHER),new Dish("pizza", true, 550, Dish.Type.OTHER),new Dish("prawns", false, 300, Dish.Type.FISH),new Dish("salmon", false, 450, Dish.Type.FISH)
);
示例1:找出低卡路里(<400)的菜品名称,并按卡路里排序
List<String> lowCaloricDishesName = menu.stream() // 1. 获取流.filter(d -> d.getCalories() < 400) // 2. 中间操作:过滤.sorted(Comparator.comparing(Dish::getCalories)) // 中间操作:排序.map(Dish::getName) // 中间操作:映射,提取菜名.collect(Collectors.toList()); // 3. 终端操作:收集为ListSystem.out.println(lowCaloricDishesName);
// 输出:[season fruit, prawns, rice]
示例2:统计菜单中有多少种肉菜
long count = menu.stream().filter(d -> d.getType() == Dish.Type.MEAT).count();
System.out.println("肉菜种类: " + count);
// 输出:肉菜种类: 3
示例3:使用 flatMap 处理嵌套集合
假设有一个 List<List>,我们想找出所有不重复的字母。
List<List<String>> listOfLists = Arrays.asList(Arrays.asList("apple", "banana"),Arrays.asList("orange", "grape", "kiwi")
);List<String> uniqueLetters = listOfLists.stream().flatMap(List::stream) // 将每个List<String>扁平化为一个String流.map(word -> word.split("")) // 将每个单词映射成字母数组 Stream<String[]>.flatMap(Arrays::stream) // 将每个数组扁平化为一个字母流 Stream<String>.distinct().collect(Collectors.toList());System.out.println(uniqueLetters);
// 输出:[a, p, l, e, b, n, o, r, g, k, i, w]
示例4:使用 reduce 求和
// 计算所有菜品的总卡路里
int totalCalories = menu.stream().map(Dish::getCalories).reduce(0, Integer::sum); // 0是初始值,Integer::sum是累加器
// 等同于 .reduce(0, (a, b) -> a + b);System.out.println("总卡路里: " + totalCalories);
五、并行流(Parallel Stream)
Stream API 简化了并行编程。只需将 .stream() 换成 .parallelStream(),或者对已有流调用 .parallel() 方法,框架会自动帮你将任务分解到多个线程上执行。
// 顺序流
long count1 = menu.stream().filter(Dish::isVegetarian).count();// 并行流
long count2 = menu.parallelStream().filter(Dish::isVegetarian).count();
注意事项:
-
状态无关:确保传递给流操作(如 filter, map)的函数是无状态的,不依赖或修改外部可变状态,否则并行时会产生错误。
-
开销:并行化本身有开销(线程创建、通信、结果合并),对于小数据量或简单操作,顺序流可能更快。
-
可拆分性:数据源是否易于分割(如 ArrayList 比 LinkedList 更容易并行)。
六、总结与最佳实践
-
优先使用声明式风格:代码更简洁、易读、易维护。
-
理解惰性求值:中间操作不触发实际计算,这允许进行优化(如短路、循环合并)。
-
合理使用并行流:对于大数据集且计算密集型的任务,考虑使用并行流。先测试,再决定。
-
熟悉 Collectors 工具类:它提供了非常多强大的收集器,如分组(groupingBy)、分区(partitioningBy)、连接字符串(joining)等,是 Stream 的利器。
Stream API 是现代 Java 编程的基石,熟练掌握它能极大提升你的开发效率和代码质量。建议多动手练习,体会其思想。