Java Stream API 学习笔记
一、 Stream API 概述
Stream API 是 Java 8 引入的强大功能,它为处理数据集合提供了一种新的范式。Stream API 可以简化代码,提高可读性,并充分利用多核处理器来提升性能。
二、 Stream API 的三大核心步骤
-
Stream Creation (流的创建)
- 流可以从多种数据源创建,包括集合、数组、文件、IO 通道等,甚至可以创建无限流。
案例:
简单案例: 从字符串列表创建流
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Eve", "Frank"); Stream<String> nameStream = names.stream();
复杂案例: 创建一个无限流,包含从 1 开始的奇数
Stream<Integer> oddNumbers = Stream.iterate(1, n -> n + 2);
-
Intermediate Operations (中间操作)
- 中间操作用于对流中的元素进行处理,例如筛选、映射、排序等。
- 中间操作是惰性执行的,这意味着它们不会立即执行,直到遇到终端操作才会实际执行。
- 每个中间操作都会返回一个新的流,从而支持链式调用,可以构建复杂的数据处理管道。
案例:
简单案例: 筛选出年龄大于 18 岁的学生,并将他们的姓名转换为大写
class Student { String name; int age; // 构造函数和 getter 方法 } List<Student> students = Arrays.asList( new Student("Alice", 18), new Student("Bob", 20), new Student("Charlie", 16), new Student("David", 22) ); Stream<String> adultNames = students.stream() .filter(student -> student.age > 18) .map(student -> student.name.toUpperCase());
复杂案例: 将包含字符串列表的流转换为包含每个字符串中所有字符的流,并去重
List<List<String>> wordGroups = Arrays.asList( Arrays.asList("a", "b", "c"), Arrays.asList("d", "e", "f"), Arrays.asList("g", "h", "i") ); Stream<String> uniqueCharacters = wordGroups.stream() .flatMap(words -> words.stream()) .distinct();
-
Terminal Operations (终端操作)
- 终端操作是整个流处理的实际执行部分,它会触发所有之前定义的中间操作,并生成最终结果。
- 执行终端操作后,流中的元素会被消费,流就不能再次被使用了。
案例:
简单案例: 计算流中所有数字的平均值
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); double average = numbers.stream().mapToInt(Integer::intValue).average().orElse(0.0);
复杂案例: 将流中的元素收集到一个 Map 中,以元素的长度为键,元素列表为值
List<String> words = Arrays.asList("apple", "banana", "cherry", "date", "elderberry"); Map<Integer, List<String>> wordsByLength = words.stream() .collect(Collectors.groupingBy(String::length));
三、 并行流
- 默认情况下,创建的流都是顺序流,但也可以通过
parallel()
方法将顺序流转换为并行流。 - 并行流可以借助多核处理器进行并行计算,从而提升数据处理的速度。
案例:
简单案例: 使用并行流计算字符串列表中所有字符串的长度总和
List<String> words = Arrays.asList("apple", "banana", "cherry", "date", "elderberry");
int totalLength = words.stream().parallel().mapToInt(String::length).sum();
复杂案例: 使用并行流对一个大型文件中的所有单词进行排序,并打印前 10 个单词
try (Stream<String> lines = Files.lines(Paths.get("large_file.txt"))) {
lines.parallel()
.flatMap(line -> Arrays.stream(line.split("\\s+")))
.sorted()
.limit(10)
.forEach(System.out::println);
} catch (IOException e) {
// 处理异常
}
四、 重要注意事项
- 流操作应该是链式调用的,即每次中间操作都应该基于前一次操作的结果,而不是重复使用同一个流实例进行多次操作。
- 流本身并不是数据结构,它不会存储数据或改变数据源,它只定义了数据处理的流程。
- 使用
Files.lines()
方法打开的文件资源必须被妥善关闭,以避免资源泄漏。为此,推荐将此操作封装在try-with-resources
语句中,这样可以自动确保无论处理过程如何,文件资源都会被关闭。
五、 其他常用操作
Collectors
工具类提供了一系列静态方法,用于创建各种常用的收集器,例如toList()
、toSet()
、toMap()
、groupingBy()
等。
案例:
简单案例: 使用 Collectors.toList()
将流中的元素收集到一个 List 中
List<String> words = Arrays.asList("apple", "banana", "cherry", "date", "elderberry");
List<String> collectedWords = words.stream().collect(Collectors.toList());
复杂案例: 使用 Collectors.toMap()
将流中的元素收集到一个 Map 中,以元素本身为键,元素的长度为值
List<String> words = Arrays.asList("apple", "banana", "cherry", "date", "elderberry");
Map<String, Integer> wordLengths = words.stream()
.collect(Collectors.toMap(word -> word, word -> word.length()));
Optional
类型用于封装可能不存在的值,例如findFirst()
和findAny()
方法返回的结果。
案例:
简单案例: 查找流中第一个长度大于 5 的字符串
List<String> words = Arrays.asList("apple", "banana", "cherry", "date", "elderberry");
Optional<String> firstLongWord = words.stream().filter(word -> word.length() > 5).findFirst();
// 使用 ifPresent() 方法检查 Optional 是否存在值
firstLongWord.ifPresent(word -> System.out.println("First long word: " + word));
六、总结
Stream API 为 Java 程序员提供了一种更强大、更简洁、更高效的方式来处理数据集合。掌握 Stream API 的核心步骤和常用操作,可以帮助你编写更优雅、更易维护的代码。