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 允许我们以多种方式创建和操作无限流,您很快就会看到。
此外,根据定义的相遇顺序,可以有序或无序。流是否有相遇顺序取决于数据源和中间操作。例如,Stream
以List
作为其源,因为List
具有内在顺序,所以对其进行排序。另一方面,Stream
以Set
作为其来源是无序的,因为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