创建流
创建流
在本教程中,您已经创建了许多流,所有这些都是通过调用 stream()
方法的 Collection
接口。此方法非常方便:以这种方式创建流只需要两行简单的代码,您可以使用此流来试验流 API 的几乎所有功能。
正如您将要看到的,还有许多其他方法可以在许多对象上创建流。了解这些方法使您能够在应用程序中的许多地方利用流 API,并编写更易读和更易维护的代码。
让我们快速浏览一下您将在本教程中看到的那些方法,然后再深入研究每个方法。
第一组模式使用来自 Stream
接口的工厂方法。使用它们,您可以从以下元素创建流
- 一个可变参数参数;
- 一个供应商;
- 一个一元运算符,它从前一个元素生成下一个元素;
- 一个构建器。
您甚至可以创建一个空流,这在某些情况下可能很方便。
您已经看到,您可以在集合上创建一个流。如果您只有迭代器而不是完整的集合,那么有一个模式适合您:您可以在迭代器上创建一个流。如果您有一个数组,那么也有一种模式可以在数组的元素上创建一个流。
它并没有止步于此。许多模式也已添加到 JDK 中的知名对象中。然后,您可以从以下元素创建流
- 字符串中的字符;
- 文本文件中的行;
- 通过使用正则表达式拆分字符字符串而创建的元素;
- 一个随机变量,可以创建一个随机数流。
您还可以使用构建器模式创建流。
从集合或迭代器创建流
您已经知道,在 Collection
接口中有一个可用的 stream()
方法。这可能是创建流最经典的方式。
在某些情况下,您可能需要在映射内容上创建一个流。在 Map
接口中没有 stream()
方法,因此您无法直接创建这样的流。但是,您可以通过三个集合访问映射的内容
- 键的集合,使用
keySet()
- 键值对的集合,使用
entrySet()
- 值的集合,使用
values()
.
要使用的正确模式是获取其中一个集合并在其上创建一个流。
流 API 为您提供了一种从简单迭代器创建流的模式。迭代器是一个非常简单的对象,因此它可能是创建非标准数据源上的流的一种非常方便的方式。模式如下。
Iterator<String> iterator = ...;
long estimateSize = 10L;
int characteristics = 0;
Spliterator<String> spliterator = Spliterators.spliterator(strings.iterator(), estimateSize, characteristics);
boolean parallel = false;
Stream<String> stream = StreamSupport.stream(spliterator, parallel);
此模式包含几个神奇的元素,将在本教程的后面部分介绍。让我们快速浏览一下它们。
estimateSize
是您认为此流将消耗的元素数量。在某些情况下,此信息很容易获得:例如,如果您正在数组或集合上创建一个流。但也有一些情况,此信息是未知的。
参数 characteristics
将在本教程的后面部分介绍。它用于优化数据的处理。
parallel
参数告诉 API 您要创建的流是并行流还是非并行流。并行流也将在本教程的后面部分介绍。
创建空流
让我们从这些模式中最简单的开始:创建空流。在 Stream
接口中有一个用于此的工厂方法。您可以按以下方式使用它。
Stream<String> empty = Stream.empty();
List<String> strings = empty.collect(Collectors.toList());
System.out.println("strings = " + strings);
运行此代码将在您的控制台上显示以下内容。
strings = []
在某些情况下,创建空流可能非常方便。事实上,您在本教程的前面部分看到了一个。您看到的模式使用空流和 flatmap 从流中删除无效元素。从 Java SE 16 开始,此模式已被 mapMulti()
模式取代。
从可变参数或数组创建流
前两种模式非常相似。第一个使用 of()
工厂方法在 Stream
接口中。第二个使用 stream()
工厂方法的 Arrays
工厂类。事实上,如果您检查 Stream.of()
方法的源代码,您会发现它调用了 Arrays.stream()
。
以下是第一个模式的实际应用。
Stream<Integer> intStream = Stream.of(1, 2, 3);
List<Integer> ints = intStream.collect(Collectors.toList());
System.out.println("ints = " + ints);
运行第一个示例将为您提供以下结果
ints = [1, 2, 3]
以下是第二个示例。
String[] stringArray = {"one", "two", "three"};
Stream<String> stringStream = Arrays.stream(stringArray);
List<String> strings = stringStream.collect(Collectors.toList());
System.out.println("strings = " + strings);
运行第二个示例将为您提供以下结果
strings = [one, two, three]
从供应商创建流
在 Stream
接口上为此有两个工厂方法。
第一个是 generate()
,它接受一个供应商作为参数。每次需要新元素时,都会调用此供应商。
您可以使用以下代码创建这样的流,但不要这样做!
Stream<String> generated = Stream.generate(() -> "+");
List<String> strings = generated.collect(Collectors.toList());
如果您运行此代码(再次,不要这样做),您会发现它永远不会停止。如果您确实运行了它并且足够耐心,您可能会看到一个 OutOfMemoryError
。如果没有,您可以通过 IDE 终止您的应用程序。此流会生成元素并且永远不会停止。它确实会生成一个无限流。
我们还没有介绍这一点,但拥有这样的流是完全合法的!您可能想知道它们有什么用?事实上,有很多。要使用它们,您需要在某个点截断此流,流 API 为您提供了多种方法来做到这一点。您已经看到了一种,还有更多即将到来。
您看到的是对该流调用 limit()
。让我们重写前面的示例并修复它。
Stream<String> generated = Stream.generate(() -> "+");
List<String> strings =
generated
.limit(10L)
.collect(Collectors.toList());
System.out.println("strings = " + strings);
运行此代码将打印以下内容。
strings = [+, +, +, +, +, +, +, +, +, +]
该 limit()
方法被称为短路方法:它可以停止对流元素的消费。您可能还记得,数据在流中一次处理一个元素:每个元素遍历您流中定义的所有操作,从第一个到最后一个。这就是此限制操作可以停止生成更多元素的原因。
从 UnaryOperator 和 Seed 创建流
如果您需要生成恒定流,则使用供应商非常棒。如果您需要一个具有不同值的无限流,那么可以使用 iterate()
模式。
此模式使用种子,它是第一个生成的元素。然后它使用 UnaryOperator
通过转换前一个元素来生成流的下一个元素。
Stream<String> iterated = Stream.iterate("+", s -> s + "+");
iterated.limit(5L).forEach(System.out::println);
您应该看到以下结果。
+
++
+++
++++
+++++
使用此模式时,请不要忘记限制流处理的元素数量。
从 Java SE 9 开始,此模式有一个重载,它将谓词作为参数。该 iterate()
方法在该谓词变为假时停止生成元素。前面的代码可以使用此模式,如下所示。
Stream<String> iterated = Stream.iterate("+", s -> s.length() <= 5, s -> s + "+");
iterated.forEach(System.out::println);
运行此代码将为您提供与上一个相同的结果。
从数字范围创建流
使用前面的模式很容易创建数字范围。但使用专门的数字流及其 range()
工厂方法更容易。
该 range()
方法采用范围的初始值和上限(不包括)。您也可以使用 rangeClosed()
方法包含上限。调用 LongStream.range(0L, 10L)
将简单地生成一个包含 0 到 9 之间所有长整数的流。
此 range()
方法也可以用于遍历数组的元素。以下是如何做到这一点。
String[] letters = {"A", "B", "C", "D"};
List<String> listLetters =
IntStream.range(0, 10)
.mapToObj(index -> letters[index % letters.length])
.collect(Collectors.toList());
System.out.println("listLetters = " + listLetters);
结果如下。
listLetters = [A, B, C, D, A, B, C, D, A, B]
基于此模式,您可以做很多事情。请注意,因为 IntStream.range()
创建一个 IntStream
,您需要使用 mapToObj()
方法将其映射到对象流。
从随机数创建流
该 Random
类用于创建随机数序列。从 Java SE 8 开始,此类中添加了几个方法来创建不同类型随机数的流:int
、long
和 double
。
您可以通过提供种子来创建 Random
的实例。此种子是一个 long
参数。随机数取决于该种子。对于给定的种子,您将始终获得相同的数字序列。这在许多情况下可能很方便,包括编写测试。在这种情况下,您可以依赖于预先知道的数字序列。
有三种方法可以生成这样的流,它们都在 Random
类中定义:ints()
、longs()
和 doubles()
。
所有这些方法都有几个重载,它们接受以下参数
- 此流将生成的元素数量;
- 生成的随机数的上限和下限。
以下是一个生成 0 到 5 之间 10 个随机整数的代码模式。
Random random = new Random(314L);
List<Integer> randomInts =
random.ints(10, 1, 5)
.boxed()
.collect(Collectors.toList());
System.out.println("randomInts = " + randomInts);
如果您使用与本示例中使用的相同的种子,您将在控制台中看到以下内容。
randomInts = [4, 4, 3, 1, 1, 1, 2, 2, 4, 2]
请注意,我们使用了在专门的数字流上可用的 boxed()
方法,它只是将此流映射到等效的包装类型流。因此,一个 IntStream
通过此方法映射到一个 Stream<Integer>
。
以下是一个生成随机布尔值流的模式。该流的任何元素都以 80% 的概率为真。
Random random = new Random(314L);
List<Boolean> booleans =
random.doubles(1_000, 0d, 1d)
.mapToObj(rand -> rand <= 0.8) // you can tune the probability here
.collect(Collectors.toList());
// Let us count the number of true in this list
long numberOfTrue =
booleans.stream()
.filter(b -> b)
.count();
System.out.println("numberOfTrue = " + numberOfTrue);
如果您使用与我们在本示例中使用的相同的种子,您将看到以下结果。
numberOfTrue = 773
您可以调整此模式以根据您的需要生成任何类型的对象。以下是一个生成包含字母 A、B、C 和 D 的流的示例。每个字母的概率如下
- A 的概率为 50%;
- B 的概率为 30%;
- C 的概率为 10%;
- D 的概率为 10%。
Random random = new Random(314L);
List<String> letters =
random.doubles(1_000, 0d, 1d)
.mapToObj(rand ->
rand < 0.5 ? "A" : // 50% of A
rand < 0.8 ? "B" : // 30% of B
rand < 0.9 ? "C" : // 10% of C
"D") // 10% of D
.collect(Collectors.toList());
Map<String, Long> map =
letters.stream()
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
map.forEach((letter, number) -> System.out.println(letter + " :: " + number));
使用相同的种子,您将获得以下结果。
A :: 470
B :: 303
C :: 117
D :: 110
使用此 groupingBy()
构建映射可能对您来说不清楚。别担心;本教程后面将介绍此模式。
从字符串的字符创建流
该 String
类在 Java SE 8 中添加了一个 chars()
方法。此方法返回一个 IntStream
,它为您提供此字符串的字符。
每个字符都作为代码点给出,它是一个整数,可能让您想起 ASCII 代码。在某些情况下,您可能需要将此整数转换为字符串,只保留此字符。
您有两种模式可以做到这一点,具体取决于您使用的 JDK 版本。
在 Java SE 10 之前,您可以使用以下代码。
String sentence = "Hello Duke";
List<String> letters =
sentence.chars()
.mapToObj(codePoint -> (char)codePoint)
.map(Object::toString)
.collect(Collectors.toList());
System.out.println("letters = " + letters);
一个 toString()
工厂方法已在 Java SE 11 中的 Character
类中添加,您可以使用它来简化此代码。
String sentence = "Hello Duke";
List<String> letters =
sentence.chars()
.mapToObj(Character::toString)
.collect(Collectors.toList());
System.out.println("letters = " + letters);
这两个代码都打印出以下内容。
letters = [H, e, l, l, o, , D, u, k, e]
从文本文件的行创建流
能够在文本文件上打开流是一个非常强大的模式。
Java I/O API 有一种模式可以从文本文件读取单行:BufferedReader.readLine()
。您可以从循环中调用此方法并逐行读取整个文本文件以进行处理。
能够使用 Stream API 处理这些行可以为您提供更易读、更易维护的代码。
有几种模式可以创建这样的流。
如果您需要重构基于使用缓冲读取器的现有代码,那么可以使用在该对象上定义的 lines()
方法。如果您正在编写新的代码来处理文本文件的内容,那么可以使用工厂方法 Files.lines()
。最后一个方法采用 Path
作为参数,并有一个重载方法,它在您正在读取的文件未以 UTF-8 编码的情况下采用 CharSet
。
您可能知道,文件资源(与任何 I/O 资源一样)在您不再需要它时应该关闭。由于您是通过 Stream API 使用此文件资源,您可能想知道如何处理它。
好消息是,Stream
接口实现了 AutoCloseable
接口。流本身是一种资源,您可以在需要时关闭它。这在您看到的内存中所有示例中实际上并不需要,但在这种情况下绝对需要。
以下是一个计算日志文件中警告数量的示例。
Path log = Path.of("/tmp/debug.log"); // adjust to fit your installation
try (Stream<String> lines = Files.lines(log)) {
long warnings =
lines.filter(line -> line.contains("WARNING"))
.count();
System.out.println("Number of warnings = " + warnings);
} catch (IOException e) {
// do something with the exception
}
try-with-resources 模式将调用流的 close()
方法,这将依次正确关闭您已解析的文本文件。
从正则表达式创建流
此系列模式的最后一个示例是在 Pattern
类中添加的方法,用于在将正则表达式应用于字符串字符后创建生成的元素的流。
假设您需要根据给定的分隔符拆分字符串。您有两种模式可以做到这一点。
- 您可以调用
String.split()
方法; - 或者,您可以使用
Pattern.compile().split()
模式。
两种模式都会为您提供一个字符串数组,其中包含分割后的结果元素。
您已经看到了从这个数组创建流的模式。让我们编写这段代码。
String sentence = "For there is good news yet to hear and fine things to be seen";
String[] elements = sentence.split(" ");
Stream<String> stream = Arrays.stream(elements);
Pattern
类也为您提供了一个方法。您可以做的是调用 Pattern.compile().splitAsStream()
。以下是您可以使用此方法编写的代码。
String sentence = "For there is good news yet to hear and fine things to be seen";
Pattern pattern = Pattern.compile(" ");
Stream<String> stream = pattern.splitAsStream(sentence);
List<String> words = stream.collect(Collectors.toList());
System.out.println("words = " + words);
运行此代码会产生以下结果。
words = [For, there, is, good, news, yet, to, hear, and, fine, things, to, be, seen]
您可能想知道这两种模式中哪一种是最好的。要回答这个问题,您需要仔细查看第一种模式。首先,您创建一个数组来存储分割的结果,然后您在这个数组上创建一个流。
第二种模式没有数组创建,因此开销更小。
您已经看到一些流可以使用短路操作(在本教程的后面部分将详细介绍这一点)。如果您有这样的流,分割整个字符串并创建结果数组可能是一个重要的但无用的开销。无法确定您的流管道是否会消耗所有元素来产生结果。
即使您的流需要消耗所有元素来产生结果,将所有这些元素存储在数组中仍然是您不需要支付的开销。
因此,在这两种情况下,使用 splitAsStream()
模式更好。它在内存方面更好,在某些情况下,在 CPU 方面也更好。
使用构建器模式创建流
使用此模式创建流是一个两步过程。首先,您将流将要消耗的元素添加到构建器中。然后,您从这个构建器中创建流。一旦您的构建器被用来创建您的流,您就不能再向其中添加更多元素,也不能再次使用它来构建另一个流。如果您这样做,您将得到一个 IllegalStateException
。
模式如下。
Stream.Builder<String> builder = Stream.<String>builder();
builder.add("one")
.add("two")
.add("three")
.add("four");
Stream<String> stream = builder.build();
List<String> list = stream.collect(Collectors.toList());
System.out.println("list = " + list);
运行此代码将打印以下内容。
list = [one, two, three, four]
在 HTTP 源上创建流
在本教程中介绍的最后一个模式是关于分析 HTTP 响应的主体。您已经看到,您可以从文本文件的行创建流,您也可以从 HTTP 响应的主体创建流。此模式由 HTTP 客户端 API 提供,该 API 添加到 JDK 11 中。
以下是它的工作原理。我们将在一个在线可用的文本上使用它:双城记,作者查尔斯·狄更斯,由古腾堡计划在线提供:https://www.gutenberg.org/files/98/98-0.txt
文本文件的开头提供了有关文本本身的信息。这本书从包含“双城记”的那一行开始。文件的末尾是分发此文件的许可证。
我们只需要书的文本,并且希望删除此分发文件的标题和页脚。
// The URI of the file
URI uri = URI.create("https://www.gutenberg.org/files/98/98-0.txt");
// The code to open create an HTTP request
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder(uri).build();
// The sending of the request
HttpResponse<Stream<String>> response = client.send(request, HttpResponse.BodyHandlers.ofLines());
List<String> lines;
try (Stream<String> stream = response.body()) {
lines = stream
.dropWhile(line -> !line.equals("A TALE OF TWO CITIES"))
.takeWhile(line -> !line.equals("*** END OF THE PROJECT GUTENBERG EBOOK A TALE OF TWO CITIES ***"))
.collect(Collectors.toList());
}
System.out.println("# lines = " + lines.size());
运行此代码将打印出以下内容。
# lines = 15904
流由您作为参数传递给 send()
方法的正文处理程序创建。HTTP 客户端 API 为您提供了多个正文处理程序。您需要以流的形式消耗正文的处理程序是由工厂方法 HttpResponse.BodyHandlers.ofLines()
创建的。这种消耗响应正文的方式非常节省内存。如果您仔细编写流,响应正文将永远不会存储在内存中。
我们决定将文本的所有行都放在一个列表中,但是,根据您需要对这些数据进行的处理,您不一定需要这样做。事实上,在大多数情况下,将这些数据存储在内存中可能是一个坏主意。
上次更新: 2021 年 9 月 14 日