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

相关文章
|
1月前
|
Java 程序员
Java编程中的异常处理:从基础到高级
在Java的世界中,异常处理是代码健壮性的守护神。本文将带你从异常的基本概念出发,逐步深入到高级用法,探索如何优雅地处理程序中的错误和异常情况。通过实际案例,我们将一起学习如何编写更可靠、更易于维护的Java代码。准备好了吗?让我们一起踏上这段旅程,解锁Java异常处理的秘密!
|
1月前
|
存储 缓存 Java
Java 并发编程——volatile 关键字解析
本文介绍了Java线程中的`volatile`关键字及其与`synchronized`锁的区别。`volatile`保证了变量的可见性和一定的有序性,但不能保证原子性。它通过内存屏障实现,避免指令重排序,确保线程间数据一致。相比`synchronized`,`volatile`性能更优,适用于简单状态标记和某些特定场景,如单例模式中的双重检查锁定。文中还解释了Java内存模型的基本概念,包括主内存、工作内存及并发编程中的原子性、可见性和有序性。
Java 并发编程——volatile 关键字解析
|
1月前
|
算法 Java 调度
java并发编程中Monitor里的waitSet和EntryList都是做什么的
在Java并发编程中,Monitor内部包含两个重要队列:等待集(Wait Set)和入口列表(Entry List)。Wait Set用于线程的条件等待和协作,线程调用`wait()`后进入此集合,通过`notify()`或`notifyAll()`唤醒。Entry List则管理锁的竞争,未能获取锁的线程在此排队,等待锁释放后重新竞争。理解两者区别有助于设计高效的多线程程序。 - **Wait Set**:线程调用`wait()`后进入,等待条件满足被唤醒,需重新竞争锁。 - **Entry List**:多个线程竞争锁时,未获锁的线程在此排队,等待锁释放后获取锁继续执行。
67 12
|
1月前
|
存储 安全 Java
Java多线程编程秘籍:各种方案一网打尽,不要错过!
Java 中实现多线程的方式主要有四种:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口和使用线程池。每种方式各有优缺点,适用于不同的场景。继承 Thread 类最简单,实现 Runnable 接口更灵活,Callable 接口支持返回结果,线程池则便于管理和复用线程。实际应用中可根据需求选择合适的方式。此外,还介绍了多线程相关的常见面试问题及答案,涵盖线程概念、线程安全、线程池等知识点。
170 2
|
2月前
|
缓存 Java 开发者
Java多线程编程的陷阱与最佳实践####
本文深入探讨了Java多线程编程中常见的陷阱,如竞态条件、死锁和内存一致性错误,并提供了实用的避免策略。通过分析典型错误案例,本文旨在帮助开发者更好地理解和掌握多线程环境下的编程技巧,从而提升并发程序的稳定性和性能。 ####
|
1月前
|
安全 算法 Java
Java多线程编程中的陷阱与最佳实践####
本文探讨了Java多线程编程中常见的陷阱,并介绍了如何通过最佳实践来避免这些问题。我们将从基础概念入手,逐步深入到具体的代码示例,帮助开发者更好地理解和应用多线程技术。无论是初学者还是有经验的开发者,都能从中获得有价值的见解和建议。 ####
|
1月前
|
Java 调度
Java中的多线程编程与并发控制
本文深入探讨了Java编程语言中多线程编程的基础知识和并发控制机制。文章首先介绍了多线程的基本概念,包括线程的定义、生命周期以及在Java中创建和管理线程的方法。接着,详细讲解了Java提供的同步机制,如synchronized关键字、wait()和notify()方法等,以及如何通过这些机制实现线程间的协调与通信。最后,本文还讨论了一些常见的并发问题,例如死锁、竞态条件等,并提供了相应的解决策略。
66 3
|
1月前
|
存储 Java 数据挖掘
Java 8 新特性之 Stream API:函数式编程风格的数据处理范式
Java 8 引入的 Stream API 提供了一种新的数据处理方式,支持函数式编程风格,能够高效、简洁地处理集合数据,实现过滤、映射、聚合等操作。
84 6
|
1月前
|
开发框架 安全 Java
Java 反射机制:动态编程的强大利器
Java反射机制允许程序在运行时检查类、接口、字段和方法的信息,并能操作对象。它提供了一种动态编程的方式,使得代码更加灵活,能够适应未知的或变化的需求,是开发框架和库的重要工具。
72 4
|
2月前
|
安全 Java 开发者
Java中的多线程编程:从基础到实践
本文深入探讨了Java多线程编程的核心概念和实践技巧,旨在帮助读者理解多线程的工作原理,掌握线程的创建、管理和同步机制。通过具体示例和最佳实践,本文展示了如何在Java应用中有效地利用多线程技术,提高程序性能和响应速度。
79 1