Java基础 - Stream 流:Stream API的中间操作

在上一篇博客中,我介绍了构建 Stream 流的多种方式,以及 Stream 流的特点和优势。如果你还没有阅读,你可以点击这里查看。

Java基础 - Stream 流:构建流的多种方式

在这篇博客中,我将探索 Stream API 的中间操作,它们可以让你对 Stream 流进行各种转换和过滤,从而得到你想要的结果。

Stream API 的中间操作是指那些返回一个新的 Stream 流对象的操作,它们不会消耗 Stream 流,也不会产生最终的结果,而是可以链式地调用,形成一个操作管道。Stream API 提供了很多中间操作,可以分为以下几类:

  • 筛选和切片:这类操作可以让你从 Stream 流中选择或者排除一些元素,例如 filter, distinct, limit, skip 等。
  • 映射:这类操作可以让你将 Stream 流中的每个元素转换为另一种类型或者形式,例如 map, flatMap, peek 等。
  • 排序:这类操作可以让你对 Stream 流中的元素进行排序,例如 sorted, reverseOrder 等。
  • 搜索:这类操作可以让你在 Stream 流中查找某些元素或者条件,例如 findAny, findFirst, anyMatch, allMatch, noneMatch 等。
  • 截断:这类操作可以让你在 Stream 流中截取某些元素或者条件,例如 takeWhile, dropWhile 等。

下面,我将用一些示例来展示这些中间操作的用法和效果。

1. 筛选和切片

1.1 filter

filter 操作可以让你根据一个谓词(Predicate)来筛选出 Stream 流中符合条件的元素,返回一个新的 Stream 流对象。例如:

// 创建一个 Stream 流对象
Stream<Integer> numberStream = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

// 使用 filter 操作筛选出偶数
Stream<Integer> evenStream = numberStream.filter(n -> n % 2 == 0);

// 输出 [2, 4, 6, 8, 10]
evenStream.forEach(System.out::println);

1.2 distinct

distinct 操作可以让你去除 Stream 流中的重复元素,返回一个新的 Stream 流对象。例如:

// 创建一个 Stream 流对象
Stream<String> animalStream = Stream.of("cat", "dog", "cat", "elephant", "dog", "fox");

// 使用 distinct 操作去除重复元素
Stream<String> uniqueStream = animalStream.distinct();

// 输出 [cat, dog, elephant, fox]
uniqueStream.forEach(System.out::println);

1.3 limit

limit 操作可以让你限制 Stream 流中的元素个数,返回一个新的 Stream 流对象。例如:

// 创建一个 Stream 流对象
Stream<String> fruitStream = Stream.of("apple", "banana", "cherry", "durian", "elderberry", "fig");

// 使用 limit 操作限制元素个数为 3
Stream<String> limitedStream = fruitStream.limit(3);

// 输出 [apple, banana, cherry]
limitedStream.forEach(System.out::println);

1.4 skip

skip 操作可以让你跳过 Stream 流中的前 n 个元素,返回一个新的 Stream 流对象。例如:

// 创建一个 Stream 流对象
Stream<String> colorStream = Stream.of("red", "green", "blue", "yellow", "pink", "purple");

// 使用 skip 操作跳过前 2 个元素
Stream<String> skippedStream = colorStream.skip(2);

// 输出 [blue, yellow, pink, purple]
skippedStream.forEach(System.out::println);

2. 映射

2.1 map

map 操作可以让你将 Stream 流中的每个元素映射为另一种类型或者形式,返回一个新的 Stream 流对象。例如:

// 创建一个 Stream 流对象
Stream<String> nameStream = Stream.of("Alice", "Bob", "Charlie", "David");

// 使用 map 操作将每个元素转换为大写
Stream<String> upperStream = nameStream.map(String::toUpperCase);

// 输出 [ALICE, BOB, CHARLIE, DAVID]
upperStream.forEach(System.out::println);

2.2 flatMap

flatMap 操作可以让你将 Stream 流中的每个元素映射为另一个 Stream 流对象,然后将这些 Stream 流对象合并为一个 Stream 流对象,返回一个新的 Stream 流对象。例如:

// 创建一个 Stream 流对象
Stream<String> sentenceStream = Stream.of("Hello world", "Java 8", "Stream API");

// 使用 flatMap 操作将每个元素分割为单词,并合并为一个 Stream 流对象
Stream<String> wordStream = sentenceStream.flatMap(s -> Stream.of(s.split(" ")));

// 输出 [Hello, world, Java, 8, Stream, API]
wordStream.forEach(System.out::println);

2.3 peek

peek 操作可以让你在 Stream 流中的每个元素上执行一个消费者(Consumer)操作,同时返回一个新的 Stream 流对象,它和原来的 Stream 流对象包含相同的元素。这个操作通常用于调试或者观察 Stream 流的中间状态。例如:

// 创建一个 Stream 流对象
Stream<Integer> numberStream = Stream.of(1, 2, 3, 4, 5);

// 使用 peek 操作在每个元素上打印一个消息
Stream<Integer> peekedStream = numberStream.peek(n -> System.out.println("Processing " + n));

// 输出 [1, 2, 3, 4, 5]
peekedStream.forEach(System.out::println);

3. 排序

3.1 sorted

sorted 操作可以让你对 Stream 流中的元素进行自然排序或者指定排序规则,返回一个新的 Stream 流对象。例如:

// 创建一个 Stream 流对象
Stream<String> animalStream = Stream.of("cat", "dog", "elephant", "fox", "giraffe");

// 使用 sorted 操作对元素进行自然排序
Stream<String> sortedStream = animalStream.sorted();

// 输出 [cat, dog, elephant, fox, giraffe]
sortedStream.forEach(System.out::println);

// 使用 sorted 操作对元素进行指定排序规则,按照长度逆序
Stream<String> reversedStream = animalStream.sorted((s1, s2) -> s2.length() - s1.length());

// 输出 [elephant, giraffe, cat, dog, fox]
reversedStream.forEach(System.out::println);

3.2 reverseOrder

reverseOrder 操作可以让你对 Stream 流中的元素进行逆序排序,返回一个新的 Stream 流对象。例如:

// 创建一个 Stream 流对象
Stream<Integer> numberStream = Stream.of(1, 2, 3, 4, 5);

// 使用 reverseOrder 操作对元素进行逆序排序
Stream<Integer> reversedStream = numberStream.reverseOrder();

// 输出 [5, 4, 3, 2, 1]
reversedStream.forEach(System.out::println);

4. 搜索

4.1 findAny

findAny 操作可以让你从 Stream 流中找到任意一个元素,返回一个 Optional 对象,它可能包含一个值,也可能为空。这个操作通常用于并行的 Stream 流,因为它不保证返回第一个元素。例如:

// 创建一个 Stream 流对象
Stream<String> animalStream = Stream.of("cat", "dog", "elephant", "fox", "giraffe");

// 使用 findAny 操作找到任意一个元素
Optional<String> anyAnimal = animalStream.findAny();

// 输出 Optional[cat] 或者其他值
System.out.println(anyAnimal);

4.2 findFirst

findFirst 操作可以让你从 Stream 流中找到第一个元素,返回一个 Optional 对象,它可能包含一个值,也可能为空。这个操作通常用于串行的 Stream 流,因为它保证返回第一个元素。例如:

// 创建一个 Stream 流对象
Stream<String> animalStream = Stream.of("cat", "dog", "elephant", "fox", "giraffe");

// 使用 findFirst 操作找到第一个元素
Optional<String> firstAnimal = animalStream.findFirst();

// 输出 Optional[cat]
System.out.println(firstAnimal);

4.3 anyMatch

anyMatch 操作可以让你判断 Stream 流中是否有任意一个元素满足一个谓词(Predicate),返回一个布尔值。例如:

// 创建一个 Stream 流对象
Stream<String> animalStream = Stream.of("cat", "dog", "elephant", "fox", "giraffe");

// 使用 anyMatch 操作判断是否有以 f 开头的元素
boolean hasF = animalStream.anyMatch(s -> s.startsWith("f"));

// 输出 true
System.out.println(hasF);

4.4 allMatch

allMatch 操作可以让你判断 Stream 流中是否所有的元素都满足一个谓词(Predicate),返回一个布尔值。例如:

// 创建一个 Stream 流对象
Stream<String> animalStream = Stream.of("cat", "dog", "elephant", "fox", "giraffe");

// 使用 allMatch 操作判断是否所有的元素都包含 a
boolean allA = animalStream.allMatch(s -> s.contains("a"));

// 输出 false
System.out.println(allA);

4.5 noneMatch

noneMatch 操作可以让你判断 Stream 流中是否没有任何一个元素满足一个谓词(Predicate),返回一个布尔值。例如:

// 创建一个 Stream 流对象
Stream<String> animalStream = Stream.of("cat", "dog", "elephant", "fox", "giraffe");

// 使用 noneMatch 操作判断是否没有以 z 结尾的元素
boolean noZ = animalStream.noneMatch(s -> s.endsWith("z"));

// 输出 true
System.out.println(noZ);

5. 截断

5.1 takeWhile

takeWhile 操作可以让你从 Stream 流中截取满足一个谓词(Predicate)的元素,直到遇到第一个不满足的元素为止,返回一个新的 Stream 流对象。例如:

// 创建一个 Stream 流对象
Stream<Integer> numberStream = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

// 使用 takeWhile 操作截取小于 5 的元素
Stream<Integer> takenStream = numberStream.takeWhile(n -> n < 5);

// 输出 [1, 2, 3, 4]
takenStream.forEach(System.out::println);

5.2 dropWhile

dropWhile 操作可以让你从 Stream 流中丢弃满足一个谓词(Predicate)的元素,直到遇到第一个不满足的元素为止,然后返回剩余的元素组成的新的 Stream 流对象。例如:

// 创建一个 Stream 流对象
Stream<Integer> numberStream = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

// 使用 dropWhile 操作丢弃小于 5 的元素
Stream<Integer> droppedStream = numberStream.dropWhile(n -> n < 5);

// 输出 [5, 6, 7, 8, 9, 10]
droppedStream.forEach(System.out::println);

6. 注意事项

6.1 只能被消费一次

一个Steam只能被消费(执行终端操作)一次。如果重复进行消费则会抛出IllegalStateException

// 创建一个 Stream 流对象
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

// 正常执行
long count = wordStream.count();

// 会抛出IllegalStateException
long anotherCount = wordStream.count();

6.2 并行流谨慎使用

Stream API 提供了并行流的支持,可以通过 parallel() 方法将顺序流转换为并行流。并行流是指可以利用多核处理器的优势,将一个流的元素分成多个数据块,然后用不同的线程并行地处理每个数据块的流。使用并行流可以提高性能,但也有一些注意事项。

  • 并行流不一定总是比顺序流快:并行流的性能受到很多因素的影响,比如数据源的可分割性,流的操作的计算成本,流的大小,硬件环境等。并行流会引入额外的开销,比如线程的分配和切换,数据的拆分和合并等。因此,并行流不适合处理小量的数据,或者涉及装箱拆箱,依赖前一个元素,有状态的操作等。在使用并行流之前,最好先进行性能测试,比较并行流和顺序流的效果,然后选择合适的方式。
  • 并行流需要考虑线程安全问题:并行流会使用默认的 ForkJoinPool 线程池来执行任务,这个线程池的大小默认是 CPU 的核心数,也可以通过设置系统属性 java.util.concurrent.ForkJoinPool.common.parallelism 来改变。如果并行流的操作涉及到共享变量的修改,或者调用了其他线程不安全的方法,就可能会导致数据的不一致或者错误。因此,在使用并行流时,要尽量避免产生副作用,或者使用线程安全的数据结构和方法,或者使用同步机制来保证线程安全。
  • 并行流可能会改变结果的顺序或者内容:并行流会将数据分成多个块,然后由不同的线程处理,这可能会导致结果的顺序和数据源的顺序不一致,或者结果的内容和数据源的内容不一致。例如,如果使用 findAny 操作在并行流中查找任意一个元素,可能会得到不同的结果,因为并行流不保证返回第一个元素。或者,如果使用 forEach 操作在并行流中遍历元素,可能会得到乱序的结果,因为并行流不保证按照数据源的顺序执行。因此,在使用并行流时,要注意选择合适的操作,或者使用 sorted 或者 forEachOrdered 等操作来保证结果的顺序。
// 创建一个 Stream 流对象
Stream<String> animalStream = Stream.of("cat", "dog", "elephant", "fox", "giraffe");

// 使用顺序流计算所有元素的长度之和
int sum1 = animalStream.mapToInt(String::length).sum();
System.out.println(sum1); // 输出 23

// 使用并行流计算所有元素的长度之和
int sum2 = animalStream.parallel().mapToInt(String::length).sum();
System.out.println(sum2); // 输出 23,和顺序流相同,因为 sum 是一个无状态的终端操作

// 使用顺序流查找任意一个以 f 开头的元素
Optional<String> any1 = animalStream.filter(s -> s.startsWith("f")).findAny();
System.out.println(any1); // 输出 Optional[fox]

// 使用并行流查找任意一个以 f 开头的元素
Optional<String> any2 = animalStream.parallel().filter(s -> s.startsWith("f")).findAny();
System.out.println(any2); // 输出 Optional[fox] 或者 Optional[fox, giraffe],因为并行流不保证返回第一个元素

// 使用顺序流遍历所有元素
animalStream.forEach(System.out::println); 
// 输出 cat, dog, elephant, fox, giraffe,和数据源的顺序相同

// 使用并行流遍历所有元素
animalStream.parallel().forEach(System.out::println); 
// 可能输出 fox, dog, cat, elephant, giraffe,和数据源的顺序不同,因为并行流不保证按照数据源的顺序执行

6.3 惰性求值

Stream API 中间操作是惰性求值的,也就是说,它们不会立即执行,而是等到遇到一个终端操作时,才会触发整个操作管道的执行。这样可以提高性能,避免不必要的计算。例如:

// 创建一个 Stream 流对象
Stream<Integer> numberStream = Stream.of(1, 2, 3, 4, 5);

// 使用 filter 和 map 中间操作,但不会立即执行
Stream<Integer> transformedStream = numberStream.filter(n -> n % 2 == 0).map(n -> n * n);

// 使用 forEach 终端操作,触发中间操作的执行
transformedStream.forEach(System.out::println); // 输出 4, 16

6.4 无状态和有状态

Stream API 中间操作可以分为无状态和有状态两种,无状态的操作是指不依赖于任何状态,只和当前元素有关的操作,例如 filter, map, peek 等;有状态的操作是指依赖于某些状态,需要考虑整个 Stream 流的元素的操作,例如 distinct, sorted, limit, skip 等。无状态的操作通常比有状态的操作更高效,因为它们不需要维护额外的状态,也不需要等待所有的元素都处理完毕。有状态的操作可能会影响并行性能,因为它们需要同步状态,或者等待所有的元素都到达。例如:

// 创建一个 Stream 流对象
Stream<String> animalStream = Stream.of("cat", "dog", "elephant", "fox", "giraffe");

// 使用 filter 和 map 无状态操作,可以并行执行
Stream<String> upperStream = animalStream.parallel().filter(s -> s.length() == 3).map(String::toUpperCase);

// 使用 distinct 和 sorted 有状态操作,需要同步状态或者等待所有元素
Stream<String> uniqueStream = animalStream.parallel().distinct().sorted();

6.5 短路和非短路

Stream API 中间操作可以分为短路和非短路两种,短路的操作是指不需要处理所有的元素,只要满足某些条件就可以停止的操作,例如 limit, takeWhile, dropWhile 等;非短路的操作是指需要处理所有的元素,不能提前停止的操作,例如 filter, map, sorted 等。短路的操作通常比非短路的操作更高效,因为它们可以减少不必要的计算,也可以和一些短路的终端操作配合,例如 anyMatch, findFirst, findAny 等。非短路的操作可能会影响性能,因为它们需要处理所有的元素,也不能和一些短路的终端操作配合。例如:

// 创建一个 Stream 流对象
Stream<Integer> numberStream = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

// 使用 limit 和 takeWhile 短路操作,可以提前停止
Stream<Integer> takenStream = numberStream.limit(5).takeWhile(n -> n < 4);

// 使用 filter 和 map 非短路操作,需要处理所有元素
Stream<Integer> filteredStream = numberStream.filter(n -> n % 2 == 0).map(n -> n * n);
  • 31
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

--土拨鼠--

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值