1.什么是 Stream API?
Stream API 是 Java 8 引入的一个用于操作集合数据的框架。它并不是数据的存储结构,而是一个高效的数据处理工具。Stream 提供了一种声明式、链式的方式,让我们能够对集合、数组等数据进行过滤、映射、排序、聚合等一系列复杂操作,并支持并行处理。
核心特点:
- 声明式代码:通过链式操作替代繁琐的循环,使代码更加简洁。
- 延迟执行:Stream 的中间操作不会立即执行,而是在需要结果时(终端操作)才会触发整个链式操作。
- 并行处理:利用多核 CPU 的优势进行并行处理,大大提升数据处理的效率。
为什么是“流式”?
流(Stream)在这里的意义是“数据流”,即对数据进行流水线式的操作。它将数据按需逐步传递,从而避免了多次遍历和中间结果存储,具有延迟执行的特点。
Stream API 可以看作是一个流式数据处理的管道,这个管道包含多个阶段的操作,数据从一个阶段流向下一个阶段,直到达到终端操作时才会执行。这样减少了中间结果的生成,从而优化了处理效率。
延迟执行
Stream 的中间操作(如 filter
和 map
)是惰性求值的,只有在终端操作(如 collect
、forEach
、reduce
)执行时,所有操作才会一并执行。这样可以避免不必要的计算,提高性能。
例如:
List<String> words = Arrays.asList("stream", "java", "lambda", "code");
List<String> result = words.stream().filter(w -> w.length() > 4).map(String::toUpperCase).collect(Collectors.toList());
在上面代码中,filter
和 map
不会立即执行,只有在 collect
被调用时,整个流才会开始执行。这种延迟执行的特点确保了只有真正需要的数据才会被处理。
延迟执行可以避免不必要的计算,优化处理效率:
- 减少数据的重复遍历:中间操作延迟进行了,然后等到最后要执行终端操作时,根据前面记录的中间操作,一次性就将所有中间操作完成,然后再执行终端操作,这样对所有元素只遍历了一次,减少遍历次数。
- 按需处理数据:中间操作先不执行,系统自动根据最后的终端操作来判断前面是否有部分的中间操作没必要执行,比如如果最后的终端操作是返回第一个元素,那之前所有的中间操作都没必要执行了。
- 动态优化操作链:可以在遍历完所有中间操作之后判断是否有些中间操作可以直接合并,这样就在一个管道流里直接完成执行,提高效率。
- 提升并行化处理的效率:在并行流中,延迟执行让 Stream 可以更灵活地根据资源和数据量进行并行优化。
2.Stream API 的作用
1.简化代码逻辑
传统 Java 中,处理集合中的数据通常需要使用 for
循环或 Iterator
迭代,这种代码冗长且难以管理。Stream API 提供了一种更简洁的链式方法调用方式,让代码更具声明性(更像是在描述“要做什么”,而不是“怎么做”),从而提升代码的可读性。
例如,将列表中的偶数提取出来并求平方:
// 使用 Stream API
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> squares = numbers.stream().filter(n -> n % 2 == 0).map(n -> n * n).collect(Collectors.toList());
相比传统循环操作,Stream API 让代码更简洁清晰。
2.提供更灵活的操作
Stream API 提供了一系列丰富的操作,如 filter
(筛选)、map
(映射)、sorted
(排序)、distinct
(去重)等。这些操作可以组合起来完成复杂的数据处理任务,而无需大量循环和条件判断。
例如,你可以在一个流操作链中同时实现筛选、转换和聚合:
int sumOfEvenSquares = numbers.stream().filter(n -> n % 2 == 0).map(n -> n * n).reduce(0, Integer::sum);
3. 支持并行处理,提高效率
Stream API 支持并行流,即可以在多核 CPU 上并行地处理数据,充分利用硬件资源。对于大规模数据处理来说,并行流可以显著提高性能。
// 并行流操作
numbers.parallelStream().filter(n -> n % 2 == 0).forEach(System.out::println);
使用并行流时,流会自动拆分成多个子任务并行处理,利用多个线程分担计算负载,从而提升效率。
3.如何创建 Stream
Stream 可以从集合、数组、范围等多种数据源创建。以下是几种常见的创建方式:
1. 从集合创建
我们可以直接从 Java 的集合框架(如 List
、Set
)中获取流。
import java.util.*;
import java.util.stream.*;public class StreamCreationExample {public static void main(String[] args) {List<String> list = Arrays.asList("apple", "banana", "orange");Stream<String> stream = list.stream(); // 创建一个顺序流Stream<String> parallelStream = list.parallelStream(); // 创建一个并行流}
}
2. 从数组创建
可以通过 Arrays.stream()
方法从数组中创建流。
String[] array = {"a", "b", "c"};
Stream<String> streamFromArray = Arrays.stream(array);
3. 使用 Stream 静态方法创建
Stream API 提供了静态方法,如 Stream.of()
、Stream.iterate()
、Stream.generate()
等,用于直接创建流。
Stream<String> streamFromValues = Stream.of("apple", "banana", "orange");Stream<Integer> infiniteStream = Stream.iterate(0, n -> n + 2); // 无限流,从 0 开始每次加 2
4. 创建数值范围流
IntStream
、LongStream
等可以创建数值范围流,适用于需要生成连续数值的场景。
IntStream intStream = IntStream.range(1, 5); // 生成 1 到 4
IntStream intStreamInclusive = IntStream.rangeClosed(1, 5); // 生成 1 到 5
3.Stream API 的操作类型
Stream 的操作分为两类:中间操作和终端操作。中间操作返回一个新的 Stream 对象(具有延迟计算特性),而终端操作触发流的执行,并返回最终结果或副作用。
延迟执行可以避免不必要的计算,优化处理效率:
- 减少数据的重复遍历:中间操作延迟进行了,然后等到最后要执行终端操作时,根据前面记录的中间操作,一次性就将所有中间操作完成,然后再执行终端操作,这样对所有元素只遍历了一次,减少遍历次数。
- 按需处理数据:中间操作先不执行,系统自动根据最后的终端操作来判断前面是否有部分的中间操作没必要执行,比如如果最后的终端操作是返回第一个元素,那之前所有的中间操作都没必要执行了。
- 动态优化操作链:可以在遍历完所有中间操作之后判断是否有些中间操作可以直接合并,这样就在一个管道流里直接完成执行,提高效率。
- 提升并行化处理的效率:在并行流中,延迟执行让 Stream 可以更灵活地根据资源和数据量进行并行优化。
1. 中间操作
中间操作是可以链式调用的操作,例如 filter
、map
、sorted
、distinct
等。中间操作是惰性求值的,即不会立即执行,而是等到执行终端操作时才会触发整个链的计算。
常见的中间操作包括:
filter
:用于筛选符合条件的元素。map
:用于将每个元素转换为新的类型。sorted
:对流中的元素进行排序。distinct
:去除流中的重复元素。limit
:限制流中元素的数量。skip
:跳过流中的前n
个元素。
2. 终端操作
终端操作会触发流的执行,并返回最终结果。常见的终端操作有:
collect
:将流的结果收集到集合、列表等。forEach
:对流中的每个元素执行操作(例如打印)。count
:计算流中元素的数量。reduce
:将流中元素合并成一个值。anyMatch
/allMatch
/noneMatch
:用于检查流中元素是否符合某种条件。
4.常见方法详解
1. filter
—— 过滤元素
filter
方法用于根据条件筛选出流中的元素,接受一个 Predicate
函数作为参数。
示例:筛选出长度大于 5 的水果名称
List<String> fruits = Arrays.asList("apple", "banana", "orange", "kiwi");List<String> filteredFruits = fruits.stream().filter(fruit -> fruit.length() > 5).collect(Collectors.toList());System.out.println(filteredFruits); // 输出:[banana, orange]
解释:
filter(fruit -> fruit.length() > 5)
通过 Lambda 表达式定义筛选条件,将长度大于 5 的元素保留在流中。collect(Collectors.toList())
将结果收集为一个List
。
2. map
—— 转换元素
map
方法用于将流中的每个元素转换成另一种形式,接收一个 Function
函数作为参数。常用于将一种类型转换为另一种类型。
示例:将所有水果名称转换为大写
List<String> fruits = Arrays.asList("apple", "banana", "orange");List<String> upperCaseFruits = fruits.stream().map(String::toUpperCase).collect(Collectors.toList());System.out.println(upperCaseFruits); // 输出:[APPLE, BANANA, ORANGE]
解释:
map(String::toUpperCase)
将每个元素转换为大写字母。collect(Collectors.toList())
将结果收集为一个List
。
3. sorted
—— 排序
sorted
方法用于对流中的元素进行排序,默认按自然顺序排序,也可以传入 Comparator
进行自定义排序。
示例:按字母顺序排序水果名称
List<String> fruits = Arrays.asList("banana", "apple", "orange");List<String> sortedFruits = fruits.stream().sorted().collect(Collectors.toList());System.out.println(sortedFruits); // 输出:[apple, banana, orange]
解释:
sorted()
按自然顺序排序。collect(Collectors.toList())
将结果收集为List
。
4. distinct
—— 去重
distinct
方法用于去除流中的重复元素,特别适合去重需求。
示例:去除数字列表中的重复项
List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 4, 4, 5);List<Integer> distinctNumbers = numbers.stream().distinct().collect(Collectors.toList());System.out.println(distinctNumbers); // 输出:[1, 2, 3, 4, 5]
解释:
distinct()
去除了重复的数字。collect(Collectors.toList())
将结果收集为List
。
5. limit
—— 限制流的元素数量
limit
用于限制流中元素的数量。例如,如果你只想获取流中的前 n
个元素,可以使用 limit
。
示例:限制水果列表只保留前 2 个元素
List<String> fruits = Arrays.asList("apple", "banana", "orange", "kiwi");List<String> limitedFruits = fruits.stream().limit(2).collect(Collectors.toList());System.out.println(limitedFruits); // 输出:[apple, banana]
解释:
limit(2)
将流中的元素限制为前 2 个。collect(Collectors.toList())
将结果收集为List
。
6. skip
—— 跳过流的元素
skip
用于跳过流中前 n
个元素,通常和 limit
搭配使用,适用于分页、截取数据等操作。
示例:跳过前 2 个水果,只保留之后的元素
List<String> fruits = Arrays.asList("apple", "banana", "orange", "kiwi");List<String> skippedFruits = fruits.stream().skip(2).collect(Collectors.toList());System.out.println(skippedFruits); // 输出:[orange, kiwi]
解释:
skip(2)
将流中的前 2 个元素跳过,保留剩下的部分。collect(Collectors.toList())
将结果收集为List
。
5.终端操作
终端操作触发流的执行,返回最终结果或产生副作用。常见的终端操作包括 collect
、forEach
、count
、reduce
等。
1. collect
—— 收集结果
collect
是最常用的终端操作之一,它用于将流中的元素收集到集合(如 List
、Set
、Map
)中,或者执行其他类型的收集操作(如统计等)。常与 Collectors
类的静态方法搭配使用。
示例:将流的结果收集为 List
List<String> fruits = Arrays.asList("apple", "banana", "orange");List<String> fruitList = fruits.stream().filter(fruit -> fruit.length() > 5).collect(Collectors.toList());System.out.println(fruitList); // 输出:[banana, orange]
解释:
collect(Collectors.toList())
将过滤后的流结果收集为一个List
。
示例:将流的结果收集为 Set
(自动去重)
List<String> fruits = Arrays.asList("apple", "banana", "orange", "apple");Set<String> fruitSet = fruits.stream().collect(Collectors.toSet());System.out.println(fruitSet); // 输出:[banana, apple, orange]
解释:
collect(Collectors.toSet())
将流结果收集为一个Set
,并自动去重。
2. forEach
—— 遍历元素
forEach
用于遍历流中的每个元素,执行指定的操作。forEach
接收一个 Consumer
,通常用来对流中的每个元素进行某种操作(如打印)。
示例:打印所有水果名称
List<String> fruits = Arrays.asList("apple", "banana", "orange");fruits.stream().forEach(System.out::println);
解释:
forEach(System.out::println)
将流中的每个元素逐一打印到控制台。
注意:
forEach
是终端操作,调用forEach
后流就会关闭,不能再次操作流。
3. count
—— 统计元素数量
count
用于计算流中的元素数量,返回一个 long
类型的值。
示例:统计水果列表中元素的数量
List<String> fruits = Arrays.asList("apple", "banana", "orange");long count = fruits.stream().count();System.out.println(count); // 输出:3
解释:
count()
直接返回流中元素的数量,不做其他操作。
4. reduce
—— 归约操作
reduce
用于将流中的元素合并成一个值,比如将数字加起来、计算乘积、拼接字符串等。reduce
接收一个 BinaryOperator
函数,适合用于聚合操作。
示例:计算数字列表中的总和
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);int sum = numbers.stream().reduce(0, Integer::sum); // 初始值为 0,逐步累加System.out.println(sum); // 输出:15
解释:
reduce(0, Integer::sum)
表示从初始值 0 开始,逐步将元素相加。Integer::sum
是BinaryOperator
实现,表示加法操作。
示例:拼接所有水果名称
List<String> fruits = Arrays.asList("apple", "banana", "orange");String concatenatedFruits = fruits.stream().reduce("", (partialString, element) -> partialString + element);System.out.println(concatenatedFruits); // 输出:applebananaorange
解释:
reduce("", (partialString, element) -> partialString + element)
将每个水果名称拼接在一起,得到完整的字符串。
5. anyMatch
、allMatch
和 noneMatch
—— 条件匹配
anyMatch
:判断流中是否有任意一个元素符合条件,返回true
或false
。allMatch
:判断流中是否所有元素都符合条件,返回true
或false
。noneMatch
:判断流中是否没有元素符合条件,返回true
或false
。
示例:判断是否存在长度大于 5 的水果名称
List<String> fruits = Arrays.asList("apple", "banana", "orange");boolean hasLongFruit = fruits.stream().anyMatch(fruit -> fruit.length() > 5);System.out.println(hasLongFruit); // 输出:true
解释:
anyMatch(fruit -> fruit.length() > 5)
表示检查流中是否有长度大于 5 的元素。
示例:判断所有水果名称是否都以小写字母开头
List<String> fruits = Arrays.asList("apple", "banana", "orange");boolean allLowerCase = fruits.stream().allMatch(fruit -> Character.isLowerCase(fruit.charAt(0)));System.out.println(allLowerCase); // 输出:true
解释:
allMatch(fruit -> Character.isLowerCase(fruit.charAt(0)))
检查流中所有元素是否都符合小写开头的条件。
6.并行流
在 Java Stream API 中,并行处理的功能可以通过 并行流 来实现。并行流会将数据自动分割成多个子流,并利用多线程进行并行计算,从而有效利用多核 CPU 的优势,提升数据处理的速度。
1.并行流的基本使用
在 Java 中,我们可以通过集合的 parallelStream()
方法或流对象的 parallel()
方法来创建并行流。并行流会将操作分发到多个线程,并在后台自动管理这些线程的任务。
通过 parallelStream()
方法创建并行流
集合类(如 List
、Set
等)提供了 parallelStream()
方法,可以直接创建一个并行流。
List<String> fruits = Arrays.asList("apple", "banana", "orange", "kiwi");fruits.parallelStream().forEach(System.out::println);
在上面的代码中,parallelStream()
方法会将 fruits
列表分割成若干子任务,然后在多个线程中并行打印每个元素。这种方式可以有效地利用多核 CPU。
通过 parallel()
方法将顺序流转换为并行流
如果你已经有一个普通的顺序流,也可以通过 parallel()
方法将其转换为并行流。
Stream<String> fruitStream = Stream.of("apple", "banana", "orange", "kiwi");fruitStream.parallel().forEach(System.out::println);
在这里,parallel()
将顺序流 fruitStream
转换成了并行流,之后的操作将会在多个线程中并行执行。
2.并行流的底层原理
并行流的实现原理是基于 Java Fork/Join 框架,它会自动将流分成多个子任务,然后在多个 CPU 核心上同时处理每个子任务。当子任务完成后,再将各自的结果合并成最终的结果。
这种方式避免了手动创建和管理线程的复杂性,由 Java 虚拟机负责线程管理,程序员只需专注于编写流操作。
3.并行流的使用示例
下面是一些实际使用并行流的场景,展示如何利用并行流来提升性能。
示例 1:计算平方和
假设有一个大规模的整数列表,我们希望计算所有整数平方的总和。并行流可以在多个线程中计算每个整数的平方,并将结果合并,显著提升计算速度。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);int sumOfSquares = numbers.parallelStream().map(n -> n * n).reduce(0, Integer::sum);System.out.println("Sum of squares: " + sumOfSquares);
在这个例子中:
parallelStream()
创建了一个并行流,将列表分为多个子任务。map(n -> n * n)
计算每个数字的平方。reduce(0, Integer::sum)
将所有平方的结果汇总成一个总和。
示例 2:过滤和统计
假设有一个包含大量数据的字符串列表,我们希望统计其中包含指定字符的字符串数量。并行流可以让每个线程处理部分数据,然后汇总结果。
List<String> words = Arrays.asList("apple", "banana", "cherry", "date", "elderberry", "fig", "grape");long count = words.parallelStream().filter(word -> word.contains("e")).count();System.out.println("Number of words containing 'e': " + count);
在这个例子中:
parallelStream()
会将列表中的数据分割,让多个线程并行执行filter(word -> word.contains("e"))
。- 最终结果是统计包含字符
e
的字符串总数。
示例 3:字符串拼接
使用并行流也可以完成字符串拼接,适用于大数据量的字符串集合处理。
List<String> words = Arrays.asList("apple", "banana", "cherry", "date", "elderberry", "fig", "grape");String result = words.parallelStream().reduce("", (partialString, element) -> partialString + element);System.out.println("Concatenated string: " + result);
在这个例子中:
parallelStream()
创建一个并行流。reduce
操作将每个字符串拼接成一个完整的字符串,多个线程并行执行字符串拼接操作。
4.并行流的注意事项
尽管并行流可以提高性能,但在某些场景下并不适用,需要注意以下几点:
-
数据量较小时不建议使用:对于小规模数据,并行流带来的线程管理开销可能会超过其带来的性能收益。
-
顺序依赖的操作不适用:某些顺序依赖操作(如列表的排序、索引访问)不适合并行流,可能会导致结果不准确。
-
非线程安全操作要谨慎:如果流操作中包含修改共享状态的操作,并行流可能导致数据冲突或不一致,应避免非线程安全的操作。
-
避免使用
forEachOrdered
:forEachOrdered
强制按顺序执行操作,可能会降低并行流的性能。