Java Stream API 是 Java 8 中引入的一个强大特性,它提供了一种高效且易于理解的数据处理方式。在处理集合时,Stream API 允许我们以声明式的方式表达复杂的数据处理操作,从而简化代码并提高效率。
Java Stream API基础
创建Stream
- 通过集合:任何集合都可以通过调用
stream()
方法来创建一个流。javaList<String> list = Arrays.asList("a", "b", "c"); Stream<String> stream = list.stream();
- 通过数组:使用
Arrays.stream(Object[])
方法。javaint[] array = {1, 2, 3}; IntStream stream = Arrays.stream(array);
- 直接创建:使用
Stream.of(T... values)
方法。javaStream<String> stream = Stream.of("a", "b", "c");
操作分类
中间操作
中间操作(Intermediate Operations)返回的是一个新的流,因此可以将其链接起来,形成一个操作链。这些操作不会立即执行,只有当终端操作被调用时,整个操作链才会开始执行。
例如,下面的代码链包含了两个中间操作map
和filter
,它们将转换元素为大写,并过滤出以"A"开头的字符串:
Stream<String> stream = list.stream()
.map(String::toUpperCase)
.filter(s -> s.startsWith("A"));
在这个例子中,直到调用终端操作之前,中间操作不会执行任何操作。
中间操作分为无状态操作和有状态操作,这两类操作在处理元素时的行为和性能特征有所不同。
无状态操作
无状态操作(Stateless Operations)不维护任何状态信息,它们处理每个元素时都是独立的,不需要考虑其他元素的状态。这意味着这些操作可以并行执行,而不会影响结果。常见的无状态操作包括:
- map:将每个元素转换成另一个形式。
- flatMap:将每个元素转换成流,然后将这些流扁平化成一个流。
- filter:过滤出满足特定条件的元素。
- peek:执行一个无副作用的操作,通常用于调试。
例如,map
操作将每个元素应用一个函数,而不会影响其他元素:
Stream<String> upperCase = list.stream().map(String::toUpperCase);
在这个例子中,每个元素都被独立地转换为大写,这个操作不需要知道其他元素的状态。
有状态操作
有状态操作(Stateful Operations)在处理元素时会维护一个或多个状态,这些状态可能会影响其他元素的处理。这意味着这些操作通常不能并行执行,或者需要特殊的处理来保证并行执行的正确性。常见的有状态操作包括:
- sorted:对元素进行排序。
- distinct:去除重复元素。
- limit:限制流的大小。
- skip:跳过流中的前几个元素。
例如,sorted
操作会对流中的所有元素进行排序,这需要知道所有元素的比较关系:
Stream<String> sorted = list.stream().sorted();
在这个例子中,为了对元素进行排序,sorted
操作需要收集所有元素并比较它们,这是一个有状态的操作。
终端操作
终端操作(Terminal Operations)会触发流的处理,并返回一个结果或产生副作用。终端操作会遍历流中的元素,并对它们应用中间操作链中定义的处理。终端操作执行后,流就被消耗掉了,不能再次使用。终端操作包括以下几种类型:
- 遍历操作:如
forEach
,用于遍历每个元素并执行一个操作。 - 收集操作:如
collect
,用于将流中的元素收集到一个集合中。 - 规约操作:如
reduce
,用于将流中的所有元素规约为一个结果。 - 查找操作:如
findFirst
、findAny
,用于查找流中的元素。 - 匹配操作:如
anyMatch
、allMatch
、noneMatch
,用于检查流中的元素是否满足某个条件。
例如,下面的代码使用了forEach
作为终端操作,它会遍历流中的每个元素,并打印出来:
stream.forEach(System.out::println);
在这个例子中,forEach
操作会立即执行,并且流中的元素会按照中间操作链定义的方式被处理和打印。
深入理解Stream操作
操作原理
流水线(Pipelining)
Java Stream的流水线设计允许将多个操作链接起来,形成一个操作链。每个操作都返回一个新的流,这样可以方便地将多个操作组合起来,而不需要创建临时集合。流水线使得Stream API易于使用且表达性强。
例如,下面的代码将多个中间操作和终端操作链接起来,形成一个流水线:
list.stream()
.filter(s -> s.startsWith("A"))
.map(String::toUpperCase)
.sorted()
.forEach(System.out::println);
在这个例子中,filter
、map
、sorted
和forEach
操作被链接起来,形成了一个操作链。
惰性求值(Lazy Evaluation)
Java Stream的中间操作是惰性求值的,这意味着它们不会立即执行。相反,它们会创建一个描述操作的流水线,只有当终端操作被调用时,这个流水线才会被实际执行。这种设计允许对整个操作链进行优化。
例如,下面的代码只创建了操作链,并没有执行任何操作:
Stream<String> stream = list.stream()
.filter(s -> s.startsWith("A"))
.map(String::toUpperCase);
直到调用终端操作,比如forEach
,上面的操作链才会被执行:
stream.forEach(System.out::println);
并行流(Parallel Streams)
并行流可以将顺序流转换为并行流,从而可能提高某些操作的执行效率。并行流利用多核处理器的并发能力,通过将任务分散到多个线程上执行,来加速数据处理。
使用并行流
使用parallel()
方法可以将顺序流转换为并行流:
Stream<String> parallelStream = list.stream().parallel();
注意事项
- 性能考量:并非所有操作都适合并行化。对于数据量较小或者操作本身开销不大的情况,并行流可能不会带来性能提升,反而可能因为线程切换和同步的开销而降低性能。
- 线程安全:在使用并行流时,需要确保操作是线程安全的。如果流操作涉及到共享状态的修改,可能需要额外的同步措施。
- 有序性:并行流可能会改变操作的顺序性。如果操作需要保持元素的顺序,应该使用顺序流或者确保并行流中的操作是顺序无关的。
高级应用
在Java Stream API的高级应用中,我们会遇到一些更复杂的数据处理场景,需要使用flatMap
和reduce
等操作来处理。同时,为了提高性能,我们需要遵循一些最佳实践,比如避免在中间操作中修改共享变量,并选择合适的流类型(并行流或顺序流)。
复杂数据处理
使用 flatMap
进行扁平化操作
flatMap
方法用于将多个流合并成一个流。当你有一个包含多个集合的流,并且想要将这些集合中的元素合并到一个流中时,flatMap
非常有用。
例如,假设我们有一个单词列表,每个单词都是一个字符串流,我们想要将这些单词的字符扁平化到一个流中:
List<String> words = Arrays.asList("hello", "world");
Stream<String> letters = words.stream()
.flatMap(word -> Stream.of(word.split("")));
使用 reduce
进行累加操作
reduce
方法用于将流中的所有元素合并成一个结果。这通常用于求和、求乘积或其他类型的累加操作。
例如,计算一个整数列表的所有元素的和:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream().reduce(0, Integer::sum);