Java 编程问题:九、函数式编程——深入研究2

简介: Java 编程问题:九、函数式编程——深入研究

Java 编程问题:九、函数式编程——深入研究1https://developer.aliyun.com/article/1426154

181 无限流、takeWhile()dropWhile()

在这个问题的第一部分,我们将讨论无限流。在第二部分中,我们将讨论takeWhile()dropWhile()api。

无限流是无限期地创建数据的流。因为流是懒惰的,它们可以是无限的。更准确地说,创建无限流是作为中间操作完成的,因此在执行管道的终端操作之前,不会创建任何数据。

例如,下面的代码理论上将永远运行。此行为由forEach()终端操作触发,并由缺少约束或限制引起:

Stream.iterate(1, i -> i + 1)
  .forEach(System.out::println);

Java 流 API 允许我们以多种方式创建和操作无限流,您很快就会看到。

此外,根据定义的相遇顺序,可以有序无序。流是否有相遇顺序取决于数据源和中间操作。例如,StreamList作为其源,因为List具有内在顺序,所以对其进行排序。另一方面,StreamSet作为其来源是无序的,因为Set不保证有序。一些中间操作(例如,sorted())可以向无序的Stream施加命令,而一些终端操作(例如,forEach())可以忽略遭遇命令。

通常,顺序流的性能不受排序的显著影响,但是取决于所应用的操作,并行流的性能可能会受到顺序Stream的存在的显著影响。

不要把Collection.stream().forEach()Collection.forEach()混为一谈。虽然Collection.forEach()可以依靠集合的迭代器(如果有的话)来保持顺序,Collection.stream().forEach()的顺序没有定义。例如,通过list.forEach()多次迭代List将按插入顺序处理元素,而list.parallelStream().forEach()在每次运行时产生不同的结果。根据经验,如果不需要流,则通过Collection.forEach()对集合进行迭代。

我们可以通过BaseStream.unordered()将有序流转化为无序流,如下例所示:

List<Integer> list 
  = Arrays.asList(1, 4, 20, 15, 2, 17, 5, 22, 31, 16);
Stream<Integer> unorderedStream = list.stream()
  .unordered();

无限有序流

通过Stream.iterate(T seed, UnaryOperator f)可以得到无限的有序流。结果流从指定的种子开始,并通过将f函数应用于前一个元素(例如,n元素是f(n-1)来继续)。

例如,类型 1、2、3、…、n 的整数流可以如下创建:

Stream<Integer> infStream = Stream.iterate(1, i -> i + 1);

此外,我们可以将此流用于各种目的。例如,让我们使用它来获取前 10 个偶数整数的列表:

List<Integer> result = infStream
  .filter(i -> i % 2 == 0)
  .limit(10)
  .collect(Collectors.toList());

List内容如下(注意无限流将创建元素 1、2、3、…、20,但只有以下元素与我们的过滤器匹配,直到达到 10 个元素的限制):

2, 4, 6, 8, 10, 12, 14, 16, 18, 20

注意limit()中间操作的存在。它的存在是强制的;否则,代码将无限期运行。我们必须显式地丢弃流;换句话说,我们必须显式地指定在最终列表中应该收集多少与我们的过滤器匹配的元素。一旦达到极限,无限流就会被丢弃。

但是假设我们不想要前 10 个偶数整数的列表,实际上我们希望直到 10(或任何其他限制)的偶数的列表。从 JDK9 开始,我们可以通过一种新的味道Stream.iterate()来塑造这种行为。这种味道让我们可以直接将hasNext谓词嵌入流声明(iterate(T seed, Predicate hasNext, UnaryOperator next)。当hasNext谓词返回false后,流即终止:

Stream<Integer> infStream = Stream.iterate(
  1, i -> i <= 10, i -> i + 1);

这一次,我们可以删除limit()中间操作,因为我们的hasNext谓词施加了 10 个元素的限制:

List<Integer> result = infStream
  .filter(i -> i % 2 == 0)
  .collect(Collectors.toList());

结果List如下(与我们的hasNext谓词一致,无限流创建元素 1、2、3、…、10,但只有以下五个元素与我们的流过滤器匹配):

2, 4, 6, 8, 10

当然,我们可以将Stream.iterate()limit()的味道结合起来形成更复杂的场景。例如,下面的流将创建新元素,直到下一个谓词i -> i <= 10。因为我们使用的是随机值,hasNext谓词返回false的时刻是不确定的:

Stream<Integer> infStream = Stream.iterate(
  1, i -> i <= 10, i -> i + i % 2 == 0 
    ? new Random().nextInt(20) : -1 * new Random().nextInt(10));

此流的一个可能输出如下:

1, -5, -4, -7, -4, -2, -8, -8, ..., 3, 0, 4, -7, -6, 10, ...

现在,下面的管道将收集最多 25 个通过infStream创建的数字:

List<Integer> result = infStream
  .limit(25)
  .collect(Collectors.toList());

现在,无限流可以从两个地方丢弃。如果hasNext谓词返回false,直到我们收集了 25 个元素,那么此时我们仍然保留收集的元素(少于 25 个)。如果直到我们收集了 25 个元素,hasNext谓词才返回false,那么limit()操作将丢弃流的其余部分。

无限伪随机值流

如果我们想要创建无限的伪随机值流,我们可以依赖于Random的方法,例如ints()longs()doubles()。例如,伪随机整数值的无限流可以声明如下(生成的整数将在[1100]范围内):

IntStream rndInfStream = new Random().ints(1, 100);

尝试获取 10 个偶数伪随机整数值的列表可以依赖于此流:

List<Integer> result = rndInfStream
  .filter(i -> i % 2 == 0)
  .limit(10)
  .boxed()
  .collect(Collectors.toList());

一种可能的输出如下:

8, 24, 82, 42, 90, 18, 26, 96, 86, 86

这一次,在收集到上述列表之前,很难说实际生成了多少个数字。

ints()的另一种味道是ints(long streamSize, int randomNumberOrigin, int randomNumberBound)。第一个参数允许我们指定应该生成多少伪随机值。例如,下面的流将在[1100]范围内正好生成 10 个值:

IntStream rndInfStream = new Random().ints(10, 1, 100);

我们可以从这 10 中取偶数值,如下所示:

List<Integer> result = rndInfStream
  .filter(i -> i % 2 == 0)
  .boxed()
  .collect(Collectors.toList());

一种可能的输出如下:

80, 28, 60, 54

我们可以使用此示例作为生成固定长度随机字符串的基础,如下所示:

IntStream rndInfStream = new Random().ints(20, 48, 126);
String result = rndInfStream
  .mapToObj(n -> String.valueOf((char) n))
  .collect(Collectors.joining());

一种可能的输出如下:

AIW?F1obl3KPKMItqY8>

Stream.ints() comes with two more flavors: one that doesn’t take any argument (an unlimited stream of integers) and another that takes a single argument representing the number of values that should be generated, that is, ints(long streamSize).

无限连续无序流

为了创建一个无限连续的无序流,我们可以依赖于Stream.generate(Supplier s)。在这种情况下,每个元素由提供的Supplier生成。这适用于生成恒定流、随机元素流等。

例如,假设我们有一个简单的助手,它生成八个字符的密码:

private static String randomPassword() {
  String chars = "abcd0123!@#$";
  return new SecureRandom().ints(8, 0, chars.length())
    .mapToObj(i -> String.valueOf(chars.charAt(i)))
    .collect(Collectors.joining());
}

此外,我们要定义一个无限顺序无序流,它返回随机密码(Main是包含前面助手的类):

Supplier<String> passwordSupplier = Main::randomPassword;
Stream<String> passwordStream = Stream.generate(passwordSupplier);

此时,passwordStream可以无限期地创建密码。但是让我们创建 10 个这样的密码:

List<String> result = passwordStream
  .limit(10)
  .collect(Collectors.toList());

一种可能的输出如下:

213c1b1c, 2badc$21, d33321d$, @a0dc323, 3!1aa!dc, 0a3##@3!, $!b2#1d@, 0@0#dd$#, cb$12d2@, d2@@cc@d

谓词返回true时执行

从 JDK9 开始,添加到Stream类中最有用的方法之一是takeWhile(Predicate predicate)。此方法具有两种不同的行为,如下所示:

  • 如果流是有序的,它将返回一个流,该流包含从该流中获取的、与给定谓词匹配的元素的最长前缀。
  • 如果流是无序的,并且此流的某些(但不是全部)元素与给定谓词匹配,则此操作的行为是不确定的;它可以自由获取匹配元素的任何子集(包括空集)。

对于有序的Stream,元素的最长前缀是流中与给定谓词匹配的连续元素序列。

注意,takeWhile()将在给定谓词返回false后丢弃剩余的流。

例如,获取 10 个整数的列表可以按如下方式进行:

List<Integer> result = IntStream
  .iterate(1, i -> i + 1)
  .takeWhile(i -> i <= 10)
  .boxed()
  .collect(Collectors.toList());

这将为我们提供以下输出:

1, 2, 3, 4, 5, 6, 7, 8, 9, 10

或者,我们可以获取随机偶数整数的List,直到第一个生成的值小于 50:

List<Integer> result = new Random().ints(1, 100)
  .filter(i -> i % 2 == 0)
  .takeWhile(i -> i >= 50)
  .boxed()
  .collect(Collectors.toList());

我们甚至可以连接takeWhile()中的谓词:

List<Integer> result = new Random().ints(1, 100)
  .takeWhile(i -> i % 2 == 0 && i >= 50)
  .boxed()
  .collect(Collectors.toList());

一个可能的输出可以如下获得(也可以为空):

64, 76, 54, 68

在第一个生成的密码不包含!字符之前,取一个随机密码的List怎么样?

根据前面列出的助手,我们可以这样做:

List<String> result = Stream.generate(Main::randomPassword)
  .takeWhile(s -> s.contains("!"))
  .collect(Collectors.toList());

一个可能的输出可以如下获得(也可以为空):

0!dac!3c, 2!$!b2ac, 1d12ba1!

现在,假设我们有一个无序的整数流。以下代码片段采用小于或等于 10 的元素子集:

Set<Integer> setOfInts = new HashSet<>(
  Arrays.asList(1, 4, 3, 52, 9, 40, 5, 2, 31, 8));
List<Integer> result = setOfInts.stream()
  .takeWhile(i -> i<= 10)
  .collect(Collectors.toList());

一种可能的输出如下(请记住,对于无序流,结果是不确定的):

1, 3, 4

谓词返回true时删除

从 JDK9 开始,我们还有Stream.dropWhile(Predicate predicate)方法。此方法与takeWhile()相反。此方法不在给定谓词返回false之前获取元素,而是在给定元素返回false之前删除元素,并在返回流中包含其余元素:

  • 如果流是有序的,则在删除与给定谓词匹配的元素的最长前缀之后,它返回一个由该流的其余元素组成的流。
  • 如果流是无序的,并且此流的某些(但不是全部)元素与给定谓词匹配,则此操作的行为是不确定的;可以随意删除匹配元素的任何子集(包括空集)。

对于有序的Stream,元素的最长前缀是流中与给定谓词匹配的连续元素序列。

例如,让我们在删除前 10 个整数后收集 5 个整数:

List<Integer> result = IntStream
  .iterate(1, i -> i + 1)
  .dropWhile(i -> i <= 10)
  .limit(5)
  .boxed()
  .collect(Collectors.toList());

这将始终提供以下输出:

11, 12, 13, 14, 15

或者,我们可以获取五个大于 50 的随机偶数整数的List(至少,这是我们认为代码所做的):

List<Integer> result = new Random().ints(1, 100)
  .filter(i -> i % 2 == 0)
  .dropWhile(i -> i < 50)
  .limit(5)
  .boxed()
  .collect(Collectors.toList());

一种可能的输出如下:

78, 16, 4, 94, 26

但为什么是 16 和 4 呢?它们是偶数,但不超过 50!它们之所以存在,是因为它们位于第一个元素之后,而第一个元素没有通过谓词。主要是当值小于 50(dropWhile(i -> i < 50)时,我们会降低值。78 值将使该谓词失败,因此dropWhile结束其作业。此外,所有生成的元素都包含在结果中,直到limit(5)采取行动。

让我们看看另一个类似的陷阱。让我们获取一个由五个随机密码组成的List,其中包含!字符(至少,我们可能认为代码就是这样做的):

List<String> result = Stream.generate(Main::randomPassword)
  .dropWhile(s -> !s.contains("!"))
  .limit(5)
  .collect(Collectors.toList());

一种可能的输出如下:

bab2!3dd, c2@$1acc, $c1c@cb@, !b21$cdc, #b103c21

同样,我们可以看到不包含!字符的密码。bab2!3dd密码将使我们的谓词失败,并最终得到最终结果(List。生成的四个密码被添加到结果中,而不受dropWhile()的影响。

现在,假设我们有一个无序的整数流。以下代码片段将删除小于或等于 10 的元素子集,并保留其余元素:

Set<Integer> setOfInts = new HashSet<>(
  Arrays.asList(5, 42, 3, 2, 11, 1, 6, 55, 9, 7));
List<Integer> result = setOfInts.stream()
  .dropWhile(i -> i <= 10)
  .collect(Collectors.toList());

一种可能的输出如下(请记住,对于无序流,结果是不确定的):

55, 7, 9, 42, 11

如果所有元素都匹配给定的谓词,那么takeWhile()接受并dropWhile()删除所有元素(不管流是有序的还是无序的)。另一方面,如果没有一个元素与给定的谓词匹配,那么takeWhile()什么也不取(返回一个空流)dropWhile()什么也不掉(返回流)。

避免在并行流的上下文中使用take/dropWhile(),因为它们是昂贵的操作,特别是对于有序流。如果适合这种情况,那么只需通过BaseStream.unordered()移除排序约束即可。


Java 编程问题:九、函数式编程——深入研究3https://developer.aliyun.com/article/1426156

相关文章
|
7天前
|
安全 Java 调度
Java编程时多线程操作单核服务器可以不加锁吗?
Java编程时多线程操作单核服务器可以不加锁吗?
21 2
|
8天前
|
并行计算 Java 测试技术
探索Java中的函数式编程
在本文中,我们将深入探讨Java中的函数式编程。我们会先了解什么是函数式编程以及为什么它如此重要。然后,通过一些简单的代码示例,展示如何在Java中应用函数式编程概念。最后,讨论在实际项目中如何利用函数式编程来提高代码的可读性和效率。
|
10天前
|
Java API 开发者
探索Java中的函数式编程
本文深入探讨了Java中的函数式编程,这是一种强调使用不可变数据和避免共享状态的编程范式。我们将从基础概念、核心特性以及实际应用案例三个方面,全面解析函数式编程在Java中的魅力和价值。
|
8天前
|
Java C语言
5-13|Java的函数式编程
5-13|Java的函数式编程
|
11天前
|
算法 安全 Java
JAVA并发编程系列(12)ThreadLocal就是这么简单|建议收藏
很多人都以为TreadLocal很难很深奥,尤其被问到ThreadLocal数据结构、以及如何发生的内存泄漏问题,候选人容易谈虎色变。 日常大家用这个的很少,甚至很多近10年资深研发人员,都没有用过ThreadLocal。本文由浅入深、并且才有通俗易懂方式全面分析ThreadLocal的应用场景、数据结构、内存泄漏问题。降低大家学习啃骨头的心理压力,希望可以帮助大家彻底掌握并应用这个核心技术到工作当中。
|
11天前
|
Java 程序员 编译器
死磕-高效的Java编程(二)
死磕-高效的Java编程(二)
|
6天前
|
Java
JAVA并发编程系列(13)Future、FutureTask异步小王子
本文详细解析了Future及其相关类FutureTask的工作原理与应用场景。首先介绍了Future的基本概念和接口方法,强调其异步计算特性。接着通过FutureTask实现了一个模拟外卖订单处理的示例,展示了如何并发查询外卖信息并汇总结果。最后深入分析了FutureTask的源码,包括其内部状态转换机制及关键方法的实现原理。通过本文,读者可以全面理解Future在并发编程中的作用及其实现细节。
|
9天前
|
Java 数据处理 调度
Java中的多线程编程:从基础到实践
本文深入探讨了Java中多线程编程的基本概念、实现方式及其在实际项目中的应用。首先,我们将了解什么是线程以及为何需要多线程编程。接着,文章将详细介绍如何在Java中创建和管理线程,包括继承Thread类、实现Runnable接口以及使用Executor框架等方法。此外,我们还将讨论线程同步和通信的问题,如互斥锁、信号量、条件变量等。最后,通过具体的示例展示了如何在实际项目中有效地利用多线程提高程序的性能和响应能力。
|
10天前
|
安全 算法 Java
Java中的多线程编程:从基础到高级应用
本文深入探讨了Java中的多线程编程,从最基础的概念入手,逐步引导读者了解并掌握多线程开发的核心技术。无论是初学者还是有一定经验的开发者,都能从中获益。通过实例和代码示例,本文详细讲解了线程的创建与管理、同步与锁机制、线程间通信以及高级并发工具等主题。此外,还讨论了多线程编程中常见的问题及其解决方案,帮助读者编写出高效、安全的多线程应用程序。
|
11天前
|
存储 缓存 Java
java线程内存模型底层实现原理
java线程内存模型底层实现原理
java线程内存模型底层实现原理
下一篇
无影云桌面