系列中的上一篇
当前教程
创建流
系列中的下一篇

系列中的上一篇: 在流上添加中间操作

系列中的下一篇: 减少流

创建流

 

创建流

在本教程中,您已经创建了许多流,所有这些都是通过调用 stream() 方法的 Collection 接口。此方法非常方便:以这种方式创建流只需要两行简单的代码,您可以使用此流来试验流 API 的几乎所有功能。

正如您将要看到的,还有许多其他方法可以在许多对象上创建流。了解这些方法使您能够在应用程序中的许多地方利用流 API,并编写更易读和更易维护的代码。

让我们快速浏览一下您将在本教程中看到的那些方法,然后再深入研究每个方法。

第一组模式使用来自 Stream 接口的工厂方法。使用它们,您可以从以下元素创建流

  • 一个可变参数参数;
  • 一个供应商;
  • 一个一元运算符,它从前一个元素生成下一个元素;
  • 一个构建器。

您甚至可以创建一个空流,这在某些情况下可能很方便。

您已经看到,您可以在集合上创建一个流。如果您只有迭代器而不是完整的集合,那么有一个模式适合您:您可以在迭代器上创建一个流。如果您有一个数组,那么也有一种模式可以在数组的元素上创建一个流。

它并没有止步于此。许多模式也已添加到 JDK 中的知名对象中。然后,您可以从以下元素创建流

  • 字符串中的字符;
  • 文本文件中的行;
  • 通过使用正则表达式拆分字符字符串而创建的元素;
  • 一个随机变量,可以创建一个随机数流。

您还可以使用构建器模式创建流。

 

从集合或迭代器创建流

您已经知道,在 Collection 接口中有一个可用的 stream() 方法。这可能是创建流最经典的方式。

在某些情况下,您可能需要在映射内容上创建一个流。在 Map 接口中没有 stream() 方法,因此您无法直接创建这样的流。但是,您可以通过三个集合访问映射的内容

要使用的正确模式是获取其中一个集合并在其上创建一个流。

流 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 开始,此类中添加了几个方法来创建不同类型随机数的流:intlongdouble

您可以通过提供种子来创建 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 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 日


系列中的上一篇
当前教程
创建流
系列中的下一篇

系列中的上一篇: 在流上添加中间操作

系列中的下一篇: 减少流