Java 8中的Java Stream指南:带有示例的深入教程

概述

Stream的添加是Java 8中添加的主要功能之一。这个深入的教程介绍了流支持的许多功能,重点是简单实用的示例。
要理解本材料,您需要具备 Java 8 的基本实用知识(lambda 表达式、Optional、方法引用)。

介绍

首先,Java 8 Streams 不应与 Java I/O 流(例如:FileInputStream 等)混淆; 这些彼此之间没有什么关系。
简而言之,流是数据源的包装器,使我们能够操作该数据源并使批量处理变得方便快捷。

流不存储数据,从这个意义上说,它不是数据结构。 它也永远不会修改底层数据源。

此功能 - java.util.stream - 支持在stream上进行函数式操作,例如集合上map-reduce转换。

在讨论术语和核心概念之前,现在让我们深入研究stream创建和使用的几个简单示例。

Java Stream创建

  1. 让我们首先从现有数组中获取stream:
private static Employee[] arrayOfEmps = {
    new Employee(1, "Jeff Bezos", 100000.0), 
    new Employee(2, "Bill Gates", 200000.0), 
    new Employee(3, "Mark Zuckerberg", 300000.0)
};

Stream.of(arrayOfEmps);
  1. 我们还可以从现有list中获取stream
private static List<Employee> empList = Arrays.asList(arrayOfEmps);
empList.stream();
  1. 我们可以使用 Stream.of() 从各个对象创建一个流:
Stream.of(arrayOfEmps[0], arrayOfEmps[1], arrayOfEmps[2]);
  1. 或者简单地使用 Stream.builder():
Stream.Builder<Employee> empStreamBuilder = Stream.builder();

empStreamBuilder.accept(arrayOfEmps[0]);

empStreamBuilder.accept(arrayOfEmps[1]);

empStreamBuilder.accept(arrayOfEmps[2]);

Stream<Employee> empStream = empStreamBuilder.build();

还有其他方法来获取流,我们将在下面的部分中看到其中一些方法。

Java Stream操作

语言Stream支持的帮助下,一些常见用法和操作。

forEach

forEach() 是最简单也是最常用的操作; 它循环遍历Stream元素,每个元素上调用supplied函数。

该方法非常常见,已经在 Iterable、Map 等中直接引入:

@Test
public void whenIncrementSalaryForEachEmployee_thenApplyNewSalary() {    
    empList.stream().forEach(e -> e.salaryIncrement(10.0));
    
    assertThat(empList, contains(
      hasProperty("salary", equalTo(110000.0)),
      hasProperty("salary", equalTo(220000.0)),
      hasProperty("salary", equalTo(330000.0))
    ));
}

这将有效地调用 empList 中每个元素的 salaryIncrement()。

map

原始Stream的每个元素调用函数后,map() 会生成一个新Stream。 新Stream可以是不同的类型。
以下示例将Integers Stream转换为员工Stream:

@Test
public void whenMapIdToEmployees_thenGetEmployeeStream() {
    Integer[] empIds = { 1, 2, 3 };
    
    List<Employee> employees = Stream.of(empIds)
      .map(employeeRepository::findById)
      .collect(Collectors.toList());
    
    assertEquals(employees.size(), empIds.length);
}

我们从数组中获取员工ID的整数Stream。每个Integer都会传递给函数employeeRepository::findById() – 该函数返回相应的Employee对象; 这有效地形成了员工Stream。

collect

我们在前面的示例中了解了collect() 的工作原理; 当我们完成所有处理后,这是从Stream中获取内容的常见方法之一:

@Test
public void whenCollectStreamToList_thenGetList() {
    List<Employee> employees = empList.stream().collect(Collectors.toList());
    
    assertEquals(empList, employees);
}

collect()对Stream实例中保存的数据元素执行可变折叠操作(将元素重新打包到某些数据结构并应用一些附加逻辑、连接它们等)。
此操作的策略是通过Collector接口实现提供的。 在上面的示例中,我们使用toList收集器将所有Stream元素收集到List实例中。

filter

接下来,我们看一下filter(); 原始Stream的元素根据指定条件判断(提供判断条件实例,是Predicate接口的实现),符合条件的元素组成一个新Stream

让我们看看它是如何工作的:

@Test
public void whenFilterEmployees_thenGetFilteredStream() {
    Integer[] empIds = { 1, 2, 3, 4 };
    
    List<Employee> employees = Stream.of(empIds)
      .map(employeeRepository::findById)
      .filter(e -> e != null)
      .filter(e -> e.getSalary() > 200000)
      .collect(Collectors.toList());
    
    assertEquals(Arrays.asList(arrayOfEmps[2]), employees);
}

在上面的示例中,我们首先过滤掉无效员工,ID为null引用,然后再次应用过滤器仅保留工资超过特定阈值的员工。

findFirst

findFirst() 返回Stream中第一个条目的Optional对象; 当然Optional可以为空:

@Test
public void whenFindFirst_thenGetFirstEmployeeInStream() {
    Integer[] empIds = { 1, 2, 3, 4 };
    
    Employee employee = Stream.of(empIds)
      .map(employeeRepository::findById)
      .filter(e -> e != null)
      .filter(e -> e.getSalary() > 100000)
      .findFirst()
      .orElse(null);
    
    assertEquals(employee.getSalary(), new Double(200000));
}

这里返回第一个工资大于100000的员工。 如果不存在这样的员工,则返回 null。

toArray

我们看到了如何使用collect()从Stream中获取数据。 如果我们需要从Stream中获取数组,我们可以简单地使用 toArray():

@Test
public void whenStreamToArray_thenGetArray() {
    Employee[] employees = empList.stream().toArray(Employee[]::new);

    assertThat(empList.toArray(), equalTo(employees));
}

语法Employee[]::new 创建一个空的Employee数组,然后用Stream中的元素填充该数组。

flatMap

stream可以保存复杂的数据结构,例如Stream<List>。 在这种情况下,flatMap() 可以帮助我们扁平化数据结构以简化进一步的操作:

@Test
public void whenFlatMapEmployeeNames_thenGetNameStream() {
    List<List<String>> namesNested = Arrays.asList( 
      Arrays.asList("Jeff", "Bezos"), 
      Arrays.asList("Bill", "Gates"), 
      Arrays.asList("Mark", "Zuckerberg"));

    List<String> namesFlatStream = namesNested.stream()
      .flatMap(Collection::stream)
      .collect(Collectors.toList());

    assertEquals(namesFlatStream.size(), namesNested.size() * 2);
}

请注意我们如何使用flatMap() API 将Stream<List>转换为更简单的Stream。

peek

我们在本节前面看到了forEach(),它是一个terminal操作。 然而,有时我们需要在应用任何terminal操作之前对stream的每个元素执行多个操作。

peek() 在这种情况下很有用。 简单来说,它对流中的每个元素执行指定的操作,并返回一个可以进一步使用的新stream。 peek()是一个intermediate操作:

@Test
public void whenIncrementSalaryUsingPeek_thenApplyNewSalary() {
    Employee[] arrayOfEmps = {
        new Employee(1, "Jeff Bezos", 100000.0), 
        new Employee(2, "Bill Gates", 200000.0), 
        new Employee(3, "Mark Zuckerberg", 300000.0)
    };

    List<Employee> empList = Arrays.asList(arrayOfEmps);
    
    empList.stream()
      .peek(e -> e.salaryIncrement(10.0))
      .peek(System.out::println)
      .collect(Collectors.toList());

    assertThat(empList, contains(
      hasProperty("salary", equalTo(110000.0)),
      hasProperty("salary", equalTo(220000.0)),
      hasProperty("salary", equalTo(330000.0))
    ));
}

这里,第一个peek() 用于增加每个员工的工资。 第二个peek() 用于打印员工。 最后使用collect()作为terminal操作。

方法类型和管道

流的操作方法类型大致分类如下

  • intermediate操作(中间)
  • terminal操作(终端)
  • short-circuiting操作(短路)
    。。。

正如我们一直在讨论的,Java stream操作分为intermediate操作和terminal操作

诸如filter()之类的intermediate操作返回一个新的stream,可以对其进行进一步的处理。 terminal操作(例如 forEach())将流标记为已使用,之后就不能再使用它了。

stream管道由流源、零个或多个intermediate操作以及terminal操作组成。

这是一个示例stream管道,其中 empList 是源,filter() 是中间操作,count 是终端操作:

@Test
public void whenStreamCount_thenGetElementCount() {
    Long empCount = empList.stream()
      .filter(e -> e.getSalary() > 200000)
      .count();

    assertEquals(empCount, new Long(1));
}

有些操作被视为short-circuiting操作。 short-circuiting操作允许无限流上的计算在有限时间内完成:

@Test
public void whenLimitInfiniteStream_thenGetFiniteElements() {
    Stream<Integer> infiniteStream = Stream.iterate(2, i -> i * 2);

    List<Integer> collect = infiniteStream
      .skip(3)
      .limit(5)
      .collect(Collectors.toList());

    assertEquals(collect, Arrays.asList(16, 32, 64, 128, 256));
}

在这里,我们使用short-circuiting操作skip()来跳过前3个元素,并使用limit()来限制使用iterate()生成的无限流中的5个元素。

稍后我们将详细讨论无限流。

惰性计算

Java stream最重要的特征之一是它们允许通过惰性求值进行显著的优化。

仅在启动终端操作时才对源数据进行计算,并且仅在需要时消耗源元素。

所有中间操作都是惰性的,因此直到实际需要处理结果时才会执行它们。

例如,考虑我们之前看到的 findFirst() 示例。 这里执行了多少次map()操作? 4 次,因为输入数组包含 4 个元素?

@Test
public void whenFindFirst_thenGetFirstEmployeeInStream() {
    Integer[] empIds = { 1, 2, 3, 4 };
    
    Employee employee = Stream.of(empIds)
      .map(employeeRepository::findById)
      .filter(e -> e != null)
      .filter(e -> e.getSalary() > 100000)
      .findFirst()
      .orElse(null);
    
    assertEquals(employee.getSalary(), new Double(200000));
}

它首先对id 1 执行所有操作。由于id 1 的工资不大于100000,因此处理继续到下一个元素。
Id 2 满足两个过滤器谓词,因此流评估终端操作 findFirst() 并返回结果。
对id 3和4不进行任何操作。
延迟处理流可以避免在不必要时检查所有数据。 当输入流是无限的而不仅仅是非常大时,这种行为变得更加重要。

基于比较的流操作

sorted

让我们从sorted()操作开始–它根据我们传入的比较器对流元素进行排序。

例如,我们可以根据员工的姓名对员工进行排序:

@Test
public void whenSortStream_thenGetSortedStream() {
    List<Employee> employees = empList.stream()
      .sorted((e1, e2) -> e1.getName().compareTo(e2.getName()))
      .collect(Collectors.toList());

    assertEquals(employees.get(0).getName(), "Bill Gates");
    assertEquals(employees.get(1).getName(), "Jeff Bezos");
    assertEquals(employees.get(2).getName(), "Mark Zuckerberg");
}

请注意,short-circuiting不会应用于sorted()。

这意味着,在上面的示例中,即使我们在Sorted()之后使用了findFirst(),所有元素的排序也会在调用findFirst()之前完成。
发生这种情况是因为在整个流排序之前操作无法知道第一个元素是什么。

min and max

顾名思义,min() 和 max() 基于比较器分别返回流中的最小和最大元素。 它们返回一个Optional,因为结果可能存在也可能不存在(例如,由于过滤):

@Test
public void whenFindMin_thenGetMinElementFromStream() {
    Employee firstEmp = empList.stream()
      .min((e1, e2) -> e1.getId() - e2.getId())
      .orElseThrow(NoSuchElementException::new);

    assertEquals(firstEmp.getId(), new Integer(1));
}

我们还可以使用 Comparator.comparing() 来避免定义比较逻辑:

@Test
public void whenFindMax_thenGetMaxElementFromStream() {
    Employee maxSalEmp = empList.stream()
      .max(Comparator.comparing(Employee::getSalary))
      .orElseThrow(NoSuchElementException::new);

    assertEquals(maxSalEmp.getSalary(), new Double(300000.0));
}

distinct

unique() 不接受任何参数并返回流中的不同元素,从而消除重复项。 它使用元素的 equals() 方法来判断两个元素是否相等:

@Test
public void whenApplyDistinct_thenRemoveDuplicatesFromStream() {
    List<Integer> intList = Arrays.asList(2, 5, 3, 2, 4, 3);
    List<Integer> distinctIntList = intList.stream().distinct().collect(Collectors.toList());
    
    assertEquals(distinctIntList, Arrays.asList(2, 5, 3, 4));
}

allMatch, anyMatch, and noneMatch

这些操作都采用predicate并返回布尔值。 一旦确定答案,就会应用短路并停止处理:

@Test
public void whenApplyMatch_thenReturnBoolean() {
    List<Integer> intList = Arrays.asList(2, 4, 5, 6, 8);
    
    boolean allEven = intList.stream().allMatch(i -> i % 2 == 0);
    boolean oneEven = intList.stream().anyMatch(i -> i % 2 == 0);
    boolean noneMultipleOfThree = intList.stream().noneMatch(i -> i % 3 == 0);
    
    assertEquals(allEven, false);
    assertEquals(oneEven, true);
    assertEquals(noneMultipleOfThree, false);
}

allMatch() 检查predicate对于流中的所有元素是否都为 true。 这里,一遇到不能被2整除的5就返回false。

anyMatch() 检查predicate对于流中的任何一个元素是否为真。 在这里,再次应用短路并在第一个元素之后立即返回 true。

noneMatch() 检查是否没有与predicate匹配的元素。 这里,只要遇到 6(可以被 3 整除)就返回 false。

Java Stream 特殊化

从我们到目前为止所讨论的来看,Stream是对象引用的流。
然而,还有IntStream、LongStream和DoubleStream——它们分别是 int、long 和 double 的原始特化。
当处理大量数值基元时,这些非常方便。

这些专用流并不扩展Stream,而是扩展BaseStream,而Stream也构建在BaseStream之上。

因此,并非Stream支持的所有操作都存在于这些流实现中。 例如,标准 min() 和 max() 采用比较器,而专用流则没有。

创建

创建 IntStream 最常见的方法是在现有流上调用 mapToInt() :

@Test
public void whenFindMaxOnIntStream_thenGetMaxInteger() {
    Integer latestEmpId = empList.stream()
      .mapToInt(Employee::getId)
      .max()
      .orElseThrow(NoSuchElementException::new);
    
    assertEquals(latestEmpId, new Integer(3));
}

在这里,我们从 Stream 开始,并通过将 Employee::getId 提供给 mapToInt 来获取 IntStream。 最后,我们调用 max() 返回最大的整数。

我们还可以使用 IntStream.of() 来创建 IntStream:

IntStream.of(1, 2, 3);

或 IntStream.range():

IntStream.range(10, 20)

它创建数字 10 到 19 的 IntStream。

在我们继续下一个主题之前,需要注意一个重要的区别:

Stream.of(1, 2, 3)

这将返回 Stream 而不是 IntStream。

类似地,使用map()而不是mapToInt()返回一个Stream而不是一个IntStream:

empList.stream().map(Employee::getId);

特殊操作

与标准流相比,专用流提供了额外的操作——这在处理数字时非常方便。
例如 sum()、average()、range() 等:

@Test
public void whenApplySumOnIntStream_thenGetSum() {
    Double avgSal = empList.stream()
      .mapToDouble(Employee::getSalary)
      .average()
      .orElseThrow(NoSuchElementException::new);
    
    assertEquals(avgSal, new Double(200000));
}

Reduction操作

归约操作(也称为折叠)采用一系列输入元素,并通过重复应用组合操作将它们组合成单个汇总结果。 我们已经看到了一些归约操作,例如 findFirst()、min() 和 max()。

让我们看看通用的 reduce() 操作的实际效果。

reduce

reduce() 最常见的形式是:

T reduce(T identity, BinaryOperator<T> accumulator)

其中identity是起始值,accumulator是我们重复应用的二元运算。

例如:

@Test
public void whenApplyReduceOnStream_thenGetValue() {
    Double sumSal = empList.stream()
      .map(Employee::getSalary)
      .reduce(0.0, Double::sum);

    assertEquals(sumSal, new Double(600000));
}

在这里,我们从初始值 0 开始,并对流的元素重复应用Double::sum()。 实际上,我们通过在Stream上应用reduce()来实现了DoubleStream.sum()。

高级 collect

我们已经看到了如何使用 Collectors.toList() 从流中获取列表。 现在让我们看看从流中收集元素的更多方法。

joining

@Test
public void whenCollectByJoining_thenGetJoinedString() {
    String empNames = empList.stream()
      .map(Employee::getName)
      .collect(Collectors.joining(", "))
      .toString();
    
    assertEquals(empNames, "Jeff Bezos, Bill Gates, Mark Zuckerberg");
}

Collectors.joining() 将在流的两个String元素之间插入分隔符。它内部使用java.util.StringJoiner来执行连接操作。

toSet

我们还可以使用 toSet() 从流元素中获取集合:


@Test
public void whenCollectBySet_thenGetSet() {
    Set<String> empNames = empList.stream()
            .map(Employee::getName)
            .collect(Collectors.toSet());
    
    assertEquals(empNames.size(), 3);
}

toCollection

我们可以使用 Collectors.toCollection() 通过传入 Supply 将元素提取到任何其他集合中。 我们还可以使用供应商的构造函数引用:

@Test
public void whenToVectorCollection_thenGetVector() {
    Vector<String> empNames = empList.stream()
            .map(Employee::getName)
            .collect(Collectors.toCollection(Vector::new));
    
    assertEquals(empNames.size(), 3);
}

这里,在内部创建了一个空集合,并对流的每个元素调用其 add() 方法。

summarizingDouble

summarizingDouble() 是另一个有趣的收集器 - 它将双生成映射函数应用于每个输入元素并返回一个包含结果值统计信息的特殊类:

@Test
public void whenApplySummarizing_thenGetBasicStats() {
    DoubleSummaryStatistics stats = empList.stream()
      .collect(Collectors.summarizingDouble(Employee::getSalary));

    assertEquals(stats.getCount(), 3);
    assertEquals(stats.getSum(), 600000.0, 0);
    assertEquals(stats.getMin(), 100000.0, 0);
    assertEquals(stats.getMax(), 300000.0, 0);
    assertEquals(stats.getAverage(), 200000.0, 0);
}

请注意我们如何分析每个员工的工资并获取该数据的统计信息 - 例如最小值、最大值、平均值等。

当我们使用专用流之一时,summaryStatistics() 可用于生成类似的结果:

@Test
public void whenApplySummaryStatistics_thenGetBasicStats() {
    DoubleSummaryStatistics stats = empList.stream()
      .mapToDouble(Employee::getSalary)
      .summaryStatistics();

    assertEquals(stats.getCount(), 3);
    assertEquals(stats.getSum(), 600000.0, 0);
    assertEquals(stats.getMin(), 100000.0, 0);
    assertEquals(stats.getMax(), 300000.0, 0);
    assertEquals(stats.getAverage(), 200000.0, 0);
}

partitioningBy

我们可以根据元素是否满足某些条件将流分成两部分。

让我们将数值数据列表分为偶数和奇数:

@Test
public void whenStreamPartition_thenGetMap() {
    List<Integer> intList = Arrays.asList(2, 4, 5, 6, 8);
    Map<Boolean, List<Integer>> isEven = intList.stream().collect(
      Collectors.partitioningBy(i -> i % 2 == 0));
    
    assertEquals(isEven.get(true).size(), 4);
    assertEquals(isEven.get(false).size(), 1);
}

在这里,流被划分为一个 Map,偶数和奇数存储为 true 和 false 键。

groupingBy

groupingBy() 提供了高级分区——我们可以将流分为两个以上的组。

它以分类函数作为参数。 该分类函数应用于流的每个元素。

该函数返回的值用作我们从 groupingBy 收集器获取的映射的键:

@Test
public void whenStreamGroupingBy_thenGetMap() {
    Map<Character, List<Employee>> groupByAlphabet = empList.stream().collect(
      Collectors.groupingBy(e -> new Character(e.getName().charAt(0))));

    assertEquals(groupByAlphabet.get('B').get(0).getName(), "Bill Gates");
    assertEquals(groupByAlphabet.get('J').get(0).getName(), "Jeff Bezos");
    assertEquals(groupByAlphabet.get('M').get(0).getName(), "Mark Zuckerberg");
}

在这个简单的示例中,我们根据员工名字的首字母对员工进行分组。

mapping

上一节中讨论的 groupingBy() 使用 Map 对流的元素进行分组。

但是,有时我们可能需要将数据分组为元素类型以外的类型。

我们可以这样做; 我们可以使用mapping(),它实际上可以使收集器适应不同的类型——使用映射函数:

@Test
public void whenStreamMapping_thenGetMap() {
    Map<Character, List<Integer>> idGroupedByAlphabet = empList.stream().collect(
      Collectors.groupingBy(e -> new Character(e.getName().charAt(0)), Collectors.mapping(Employee::getId, Collectors.toList())));

    assertEquals(idGroupedByAlphabet.get('B').get(0), new Integer(2));
    assertEquals(idGroupedByAlphabet.get('J').get(0), new Integer(1));
    assertEquals(idGroupedByAlphabet.get('M').get(0), new Integer(3));
}

这里,mapping() 使用 getId() 映射函数将流元素 Employee 映射到员工 ID(一个整数)。 这些 ID 仍根据员工名字的首字母进行分组。

reducing

reduce() 与我们之前探讨过的reduce() 类似。 它只是返回一个收集器,该收集器执行其输入元素的减少:

@Test
public void whenStreamReducing_thenGetValue() {
    Double percentage = 10.0;
    Double salIncrOverhead = empList.stream().collect(Collectors.reducing(
        0.0, e -> e.getSalary() * percentage / 100, (s1, s2) -> s1 + s2));

    assertEquals(salIncrOverhead, 60000.0, 0);
}

这里reducing()获取每个员工的工资增量并返回总和。

当在 groupingBy() 或 partitioningBy() 的下游用于多级缩减时,reducing() 最有用。 要对流执行简单的归约,请改用reduce()。

例如,让我们看看如何将reducing()与groupingBy()结合使用:

@Test
public void whenStreamGroupingAndReducing_thenGetMap() {
    Comparator<Employee> byNameLength = Comparator.comparing(Employee::getName);
    
    Map<Character, Optional<Employee>> longestNameByAlphabet = empList.stream().collect(
      Collectors.groupingBy(e -> new Character(e.getName().charAt(0)),
        Collectors.reducing(BinaryOperator.maxBy(byNameLength))));

    assertEquals(longestNameByAlphabet.get('B').get().getName(), "Bill Gates");
    assertEquals(longestNameByAlphabet.get('J').get().getName(), "Jeff Bezos");
    assertEquals(longestNameByAlphabet.get('M').get().getName(), "Mark Zuckerberg");
}

在这里,我们根据员工名字的首字母对员工进行分组。 在每个组中,我们找到名字最长的员工。

Parallel Streams

利用对并行流的支持,我们可以并行执行流操作,而无需编写任何样板代码; 我们只需将流指定为并行:

@Test
public void whenParallelStream_thenPerformOperationsInParallel() {
    Employee[] arrayOfEmps = {
      new Employee(1, "Jeff Bezos", 100000.0), 
      new Employee(2, "Bill Gates", 200000.0), 
      new Employee(3, "Mark Zuckerberg", 300000.0)
    };

    List<Employee> empList = Arrays.asList(arrayOfEmps);
    
    empList.stream().parallel().forEach(e -> e.salaryIncrement(10.0));
    
    assertThat(empList, contains(
      hasProperty("salary", equalTo(110000.0)),
      hasProperty("salary", equalTo(220000.0)),
      hasProperty("salary", equalTo(330000.0))
    ));
}

这里,通过简单地添加parallel()语法,salaryIncrement()将在流的多个元素上并行执行。

当然,如果您需要对操作的性能特征进行更多控制,则可以进一步调整和配置此功能。

与编写多线程代码的情况一样,我们在使用并行流时需要注意一些事情:

  1. 我们需要确保代码是线程安全的。 如果并行执行的操作修改共享数据,则需要特别小心。
  2. 如果执行操作的顺序或输出流中返回的顺序很重要,那么我们不应该使用并行流。 例如,在并行流的情况下,像findFirst()这样的操作可能会生成不同的结果。
  3. 另外,我们应该确保代码并行执行是值得的。 了解操作的性能特征,以及整个系统的性能特征,这自然非常重要。

Infinite Streams

有时,我们可能想在元素仍在生成时执行操作。 我们可能事先不知道需要多少元素。 与使用列表或映射(其中所有元素都已填充)不同,我们可以使用无限流,也称为无界流。

有两种方法可以生成无限流:

generate

我们为generate()提供了一个Supplier,每当需要生成新的流元素时就会调用它:

@Test
public void whenGenerateStream_thenGetInfiniteStream() {
    Stream.generate(Math::random)
      .limit(5)
      .forEach(System.out::println);
}

在这里,我们将 Math::random() 作为供应商传递,它返回下一个随机数。

对于无限流,我们需要提供一个条件来最终终止处理。 一种常见的方法是使用 limit()。 在上面的示例中,我们将流限制为 5 个随机数并在生成时打印它们。

请注意,传递给generate()的Supplier可能是有状态的,并且这样的流在并行使用时可能不会产生相同的结果。

iterate

iterate() 采用两个参数:一个初始值(称为种子元素)和一个使用前一个值生成下一个元素的函数。 iterate() 根据设计,是有状态的,因此在并行流中可能没有用:

@Test
public void whenIterateStream_thenGetInfiniteStream() {
    Stream<Integer> evenNumStream = Stream.iterate(2, i -> i * 2);

    List<Integer> collect = evenNumStream
      .limit(5)
      .collect(Collectors.toList());

    assertEquals(collect, Arrays.asList(2, 4, 8, 16, 32));
}

在这里,我们传递 2 作为种子值,它成为流的第一个元素。 该值作为输入传递给 lambda,返回 4。该值又作为下一次迭代中的输入传递。

这一直持续到我们生成由 limit() 指定的元素数量为止,该元素充当终止条件。

文件操作

让我们看看如何在文件操作中使用流。

文件写操作

@Test
public void whenStreamToFile_thenGetFile() throws IOException {
    String[] words = {
      "hello", 
      "refer",
      "world",
      "level"
    };
    
    try (PrintWriter pw = new PrintWriter(Files.newBufferedWriter(Paths.get(fileName)))) {
        Stream.of(words).forEach(pw::println);
    }
}

这里我们使用 forEach() 通过调用 PrintWriter.println() 将流的每个元素写入文件。

文件读取操作

private List<String> getPalindrome(Stream<String> stream, int length) {
    return stream.filter(s -> s.length() == length)
      .filter(s -> s.compareToIgnoreCase(new StringBuilder(s).reverse().toString()) == 0)
      .collect(Collectors.toList());
}

@Test
public void whenFileToStream_thenGetStream() throws IOException {
    List<String> str = getPalindrome(Files.lines(Paths.get(fileName)), 5);
    assertThat(str, contains("refer", "level"));
}

这里 Files.lines() 以 Stream 的形式返回文件中的行,该 Stream 由 getPalindrome() 消耗以进行进一步处理。

getPalindrome() 在流上工作,完全不知道流是如何生成的。 这也提高了代码的可重用性并简化了单元测试。

参考文献

A Guide to Java Streams in Java 8: In-Depth Tutorial With Examples

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值