1. 引言
大家好!欢迎来到本系列博客的第三篇。在前两篇文章中,我们已经领略了 Java 8 中 行为参数化 和 Lambda 表达式 的魅力。
- 在第 1 章 Java行为参数化:从啰嗦到简洁的代码进化中,我们了解到如何通过将行为(代码块)作为参数传递给方法,使代码更灵活、可复用。
- 在第 2 章 Java 8 Lambda表达式详解:从入门到实践中,我们深入学习了 Lambda 表达式,它是实现行为参数化的简洁而强大的工具。
强烈建议先阅读前两篇文章,它们为理解今天的主题——Java 8 中的“流”(Streams)——奠定了基础。
那么,什么是“流”?它为何如此重要?
简而言之,Java 8 的“流”提供了一种全新的、声明式的处理数据的方式。它允许你以类似于 SQL 查询的风格操作集合(及其他数据源),无需编写冗长的循环和条件语句。
想象一下工厂的流水线:原材料(数据)从一端进入,经过一系列处理工序(操作),最终产出成品。Java 8 中,“流”就像这条流水线,数据在其中流动,我们可以通过各种“流操作”对其进行 筛选
、转换
、排序
、分组
等。
本篇我们将深入探讨“流”的方方面面:
- 流的定义
- 流的特性
- 流与集合的区别
- 流的核心操作
- 如何利用流编写更简洁、高效、易于理解的代码
让我们一起开启 Java 8“流”的探索之旅!
2. 流是什么?(What are Streams?)
引言中,我们用流水线类比了“流”。现在,让我们揭开“流”的神秘面纱。
流是“从支持数据处理操作的源生成的一系列元素”。
——《Java 8 in Action》
让我们拆解这个定义:
-
一系列元素: 与集合类似,流也是一系列元素的集合。你可以把一堆苹果放进篮子(集合),也可以把它们放在流水线(流)上。关键在于,流关注的是如何处理这些元素,而不是如何存储它们。
-
源: 流中的元素从哪里来?答案是“源”。它可以是:
- 集合 (List, Set 等)
- 数组
- I/O 资源 (文件等)
- 生成函数 (例如,产生无限序列的函数) 流本身不存储数据,它只是从源头获取数据。
-
数据处理操作: 这是流的核心!流提供了一套丰富的操作,让你对数据进行各种处理,类似数据库查询操作:
filter
: 筛选符合条件的元素。map
: 将元素转换为另一种形式(如小写字母转大写)。reduce
: 将所有元素组合成一个结果(如求和)。sort
: 排序。- … 还有很多!
-
内部迭代: 通常,我们用
for
循环或forEach
显式遍历集合(外部迭代)。而流则不同,它在内部迭代。你只需要告诉流_你想要做什么_,无需关心_如何做_。这使代码更简洁,也更容易优化(如并行处理)。
流不是新的数据结构,而是更高层次的抽象。它专注于 做什么(数据处理),而不是 怎么做(迭代细节)。流像管道,数据从源头流入,经过一系列处理,产生结果。这种声明式编程风格使代码更易读、维护。
3. 流与集合(Streams vs. Collections)
Java 8 的「流」常与集合(Collections)比较。虽都用于处理数据,但两者差异显著。理解这些差异对于有效使用流至关重要。
相同点:
- 存储元素: 流和集合都可存储一系列元素。
不同点:
特性 | 集合 (Collections) | 流 (Streams) |
---|---|---|
主要目的 | 存储和访问元素 | 对元素进行计算 |
何时计算 | 元素在加入集合时就已计算好 | 元素在需要时才计算(延迟计算/惰性求值) |
迭代方式 | 外部迭代(用户代码控制迭代) | 内部迭代(流库自身控制迭代) |
遍历次数 | 可以多次遍历 | 只能遍历一次 |
数据修改 | 可以添加、删除、修改集合中的元素 | 流操作通常不修改数据源 |
数据结构 | 是一种数据结构,主要目的是以特定的时间/空间复杂度存储和访问数据 | 不是数据结构,它没有存储空间,主要目的是对数据源进行计算。 |
详细解释几个关键区别:
3.1 只能遍历一次
这是流的重要限制。一旦对流执行终端操作(如 forEach
、collect
),流就被“消费”,不能再用。再次遍历会抛 IllegalStateException
。
代码示例:
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
Stream<String> nameStream = names.stream();// 第一次遍历:打印名字
nameStream.forEach(System.out::println);// 第二次遍历:会抛出异常!
// nameStream.forEach(System.out::println); // java.lang.IllegalStateException: stream has already been operated upon or closed
这与集合形成对比,集合可多次遍历。
3.2 外部迭代与内部迭代
- 外部迭代(集合): 编写显式循环(如
for-each
)遍历集合,并处理元素。你完全掌控迭代过程。 - 内部迭代(流): 只需告诉流你想做什么(如筛选长度大于3的名字),流内部进行迭代和处理。无需编写循环,代码更简洁。
代码示例:
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");// 外部迭代(集合)
List<String> longNames1 = new ArrayList<>();
for (String name : names) {if (name.length() > 3) {longNames1.add(name);}
}
System.out.println(longNames1); // [Alice, Charlie, David]// 内部迭代(流)
List<String> longNames2 = names.stream().filter(name -> name.length() > 3).collect(Collectors.toList());
System.out.println(longNames2); // [Alice, Charlie, David]
流(内部迭代)代码更简洁、易读,更接近声明式编程。我们描述了想要什么(筛选长度大于3的名字),未指定如何做(循环和条件判断)。
3.3 延迟计算/惰性求值
这是流的重要特性。流的中间操作(如filter
,map
)延迟计算。遇到终端操作前,中间操作不执行。终端操作触发时,才计算。
4. 流操作详解 (Stream Operations in Detail)
流的强大在于其丰富的操作,让你以声明式方式处理数据。操作分两类:中间操作和终端操作。理解这两类操作及如何协同工作,是掌握流的关键。
4.1 中间操作 (Intermediate Operations)
特点:
- 返回另一个流: 每个中间操作返回新流。可将多个中间操作链接,形成“流水线”。
- 延迟执行(Lazy): 中间操作不立即执行,只构建流水线。终端操作触发时,中间操作才执行。
常见中间操作:
操作 | 描述 | 示例 |
---|---|---|
filter | 筛选符合条件的元素 | stream.filter(x -> x > 5) |
map | 将每个元素映射为另一个元素(类型可能不同) | stream.map(String::toUpperCase) |
limit | 截取流的前 N 个元素 | stream.limit(10) |
skip | 跳过流的前 N 个元素 | stream.skip(5) |
distinct | 去除流中的重复元素(根据 equals ) | stream.distinct() |
sorted | 对流中的元素排序(自然排序或根据 Comparator ) | stream.sorted() stream.sorted(Comparator.reverseOrder()) |
peek | 对流中每个元素执行一个操作,但不改变流内容(主要用于调试) | stream.peek(System.out::println) |
flatMap | 将每个元素转换为一个流,然后将这些流合并为一个流。 | stream.flatMap(Collection::stream) |
代码示例 (中间操作链):
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Eve");List<String> result = names.stream().filter(name -> name.length() > 3) // 筛选长度大于3的名字.map(String::toLowerCase) // 转小写.sorted() // 排序.collect(Collectors.toList()); // 收集结果System.out.println(result); // [alice, charlie, david]
filter
、map
、sorted
是中间操作。它们链接成流水线。注意,直到 collect
(终端操作)被调用,中间操作才执行。
4.2 终端操作 (Terminal Operations)
特点:
- 产生结果或副作用: 终端操作触发流水线执行,产生结果(非流值)或副作用(如打印)。
- 消费流: 终端操作执行后,流被消费,不能再用。
常见终端操作:
操作 | 描述 | 示例 |
---|---|---|
forEach | 对流中每个元素执行一个操作(副作用) | stream.forEach(System.out::println) |
count | 返回流中元素个数 | long count = stream.count() |
collect | 将流中元素收集到集合(或其他数据结构) | List<String> list = stream.collect(Collectors.toList()) |
reduce | 将流中元素组合成一个值(如求和、求最大值) | Optional<Integer> sum = stream.reduce(Integer::sum) |
anyMatch | 检查是否至少有一个元素匹配给定条件 | boolean hasLongName = stream.anyMatch(s -> s.length() > 5) |
allMatch | 检查是否所有元素都匹配给定条件 | boolean allUpperCase = stream.allMatch(s -> Character.isUpperCase(s.charAt(0))) |
noneMatch | 检查是否没有元素匹配给定条件 | boolean noEmptyString = stream.noneMatch(String::isEmpty) |
findFirst | 返回流中第一个元素(Optional) | Optional<String> first = stream.findFirst() |
findAny | 返回流中任意一个元素(Optional,并行流中更常用) | Optional<String> any = stream.findAny() |
代码示例 (终端操作):
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);// 求和
int sum = numbers.stream().reduce(0, Integer::sum); // 初始值为0,用 Integer.sum() 累加
System.out.println("Sum: " + sum); // Sum: 15// 查找第一个偶数
Optional<Integer> firstEven = numbers.stream().filter(n -> n % 2 == 0).findFirst();
firstEven.ifPresent(System.out::println); // 2 (若存在偶数)// 检查是否所有数字都大于0
boolean allPositive = numbers.stream().allMatch(n -> n > 0);
System.out.println("All positive: " + allPositive); // All positive: true
5. 流的“按需计算”(On-Demand Computation)
前面多次提到流的“延迟计算”/“惰性求值”。现在深入探讨。
5.1 什么是“按需计算”?
流中元素只在真正需要时才计算。与集合对比,集合中所有元素在创建时就已存在于内存。
5.2 为什么“按需计算”重要?
带来几个关键优势:
-
效率提升: 若非所有元素都需处理,“按需计算”可避免不必要计算,提高效率。处理大数据集时,优势明显。
-
短路操作: “按需计算”使“短路操作”(如
findFirst
、anyMatch
)成为可能。找到满足条件的元素,就无需处理剩余元素。 -
无限流: “按需计算”使创建“无限流”(Infinite Streams)成为可能。无限流无固定结尾,可根据需要生成无限多元素。
5.3 “按需计算”如何工作?
通过中间操作和终端操作协同实现。
- 中间操作: “懒惰”。只构建处理流水线,不立即执行。
- 终端操作: “急切”。终端操作被调用,触发流水线执行。
终端操作需要元素时,流水线上中间操作才处理数据源。中间操作通常非一次处理一个元素,而是按需逐个处理。
代码示例(演示“按需计算”):
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);Optional<Integer> firstEvenGreaterThan5 = numbers.stream().filter(n -> {System.out.println("Filtering: " + n); // 打印过滤操作的中间结果return n % 2 == 0;}).filter(n -> {System.out.println("Filtering again: "+n);return n > 5;}).findFirst();firstEvenGreaterThan5.ifPresent(n -> System.out.println("Result: " + n));
输出:
Filtering: 1
Filtering: 2
Filtering again: 2
Filtering: 3
Filtering: 4
Filtering again: 4
Filtering: 5
Filtering: 6
Filtering again: 6
Result: 6
分析:
从输出可见:
- 并非所有数字都被
filter
处理。 findFirst
找到第一个满足条件的元素(6),后续元素不再处理。- 两个
filter
非独立,而是交替执行。
这就是“按需计算”。流只处理必要元素,找到 findFirst
要求的结果。
6.总结
Java 8 的流(Streams)是一种强大而优雅的数据处理工具。它通过声明式、函数式的风格,使代码更简洁、易读、高效。
在这篇文章中,我们深入探讨了:
- 流的本质: 一种支持数据处理操作的元素序列,强调“做什么”而非“怎么做”。
- 流与集合的区别: 延迟计算、内部迭代、一次性遍历等。
- 流的操作: 中间操作(构建流水线)和终端操作(触发计算)。
- 按需计算: 流的关键特性,提高效率、支持短路操作和无限流。
掌握了流,你就掌握了 Java 8 中最强大的武器之一。在后续的文章中,我们会进一步探索流的高级用法,包括并行流、自定义收集器等。敬请期待!