Java Stream API 实用指南

83 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 6 天,点击查看活动详情

StreamAPI 是在 Java 8 中引入的。它提供了一种声明式编程方法来迭代和执行集合上的操作。直到 Java 7,for并且for each是唯一可用的选项,这是一种命令式编程方法。在本文中,我将向您介绍StreamAPI 以及它如何提供对集合上执行的常见操作的抽象。

使用时命令式编程,开发人员使用语言结构来编写做什么和怎么做. 而在使用时声明式编程, 开发人员只需专注于定义该怎么办语言或框架负责怎么做部分。因此在声明式编程中,代码简洁且不易出错。

在示例中,我将使用一组Person对象。为了便于理解,Person类的定义如下所示。

public class Person {
    private final String name;
    private final int age;
    private final Gender gender;    public Person(String name, int age, Gender gender) {
        this.name = name;
        this.age = age;
        this.gender = gender;
    }    
    public String getName() { return name; }   
    public int getAge() { return age; }    
    public Gender getGender() { return gender; }
}
public enum Gender {
    MALE, FEMALE, OTHER
}

Stream API 简介

在深入研究使用 Stream API 对集合执行操作的示例之前,让我们使用一个示例来了解 Stream API 本身。

List<Person> people = ...// bulding a stream
List<String> namesOfPeopleBelow20 = people.stream()  
    // pipelining a computation
    .filter(person -> person.getAge() < 20)  
    // pipelining another computation
    .map(Person::getName)  
    // terminating a stream
    .collect(Collectors.toList());

在上面的示例中,多个操作链接在一起形成类似于处理管道的东西。这就是我们所说的流管道。流管道由以下三部分组成:

  1. 流构建器——在上面的示例中,我们有一个由Person表示peoplestream()在 Java 8的接口上添加的Collection被调用people以构建流。除了 Collection 之外,流的常见来源是数组 (Arrays.stream()) 和生成器函数 (Stream.iterate()Stream.generate())。
  2. 中间操作——创建流对象后,您可以通过链接操作在流上应用零个、一个或多个操作,就像在构建器模式中一样。您在上面的示例中看到的所有方法,filtermap,都是Stream接口上的方法,它们返回自身的实例Stream以启用链接。当操作返回Stream自身时,它们被称为中间操作。
  3. 终端操作——应用所有计算后,您可以通过应用强制终端运算符来完成管道。终端运算符也是Stream接口上的方法,并返回不是 Stream 的结果类型。在上面的例子中,collect(Collectors.toList())返回一个实例List。根据使用的终端操作,结果类型可能是也可能不是 Collection。它可以是原始值或不是集合的对象的实例。

现在让我们看看我们可以使用 Stream 执行的基本操作。虽然我们将学习单独应用于 Stream 的操作,但您始终可以混合和匹配它们以获得不同的结果。

转型

转换意味着转换存储在集合的每个元素中的值的类型。假设我们要从人员集合中派生出一组人名。在这种情况下,我们必须使用转换操作将人转换为姓名。

在下面的示例中,我们使用map中间运算符转换PeopleString持有人名的。Person::getName方法引用,等效于person -> person.getName()并且是Function的实例。

List<String> namesOfPeople = people.stream()  
    .map(Person::getName)  
    .collect(Collectors.toList());  
}

过滤

顾名思义,只有当对象满足Predicate所设定的条件时,过滤操作才允许对象流过自身。过滤器运算符Predicate在应用于 Stream 之前由它组成。

过滤也可以被认为是根据计数选择少数元素。Stream API为此提供了skip()和运算符。limit()

在下面的第一个示例中,person -> person.getAge() < 20Predicate 用于构建一个仅包含 20 岁以下人员的集合。在下面的第二个示例中,在跳过前 2 个人后选择了 10 个人。

// filtering using Predicate
List<Person> listOfPeopleBelow20 = people.stream() 
    .filter(person -> person.getAge() < 20)  
    .collect(Collectors.toList());// count based filtering    
List<Person> smallerListOfPeople = people.stream()
    .skip(2)
    .limit(10)
    .collect(Collectors.toList());

重新排序

如果要对集合中的元素进行排序,可以使用sorted中间运算符。它需要一个Comparator接口的实例。为了创建实例,我comparingComparator. 如果您有兴趣了解更多相关信息,请查看此链接

在下面的示例中,结果集合按年龄降序排序。

List<Person> peopleSortedEldestToYoungest = people.stream()  
    .sorted(Comparator.comparing(Person::getAge).reversed())  
    .collect(Collectors.toList());

与我们迄今为止看到的其他操作不同,该sorted操作是有状态的。这意味着操作员必须先查看流中的所有元素,然后才能将排序结果提供给进一步的中间或终端操作员。这种运算符的另一个例子是distinct

总结

有时您希望从集合中获取信息。例如,导出所有人的年龄总和。在StreamAPI 中,这是使用终端操作符实现的。reduce并且collect是为此目的提供的通用终端运营商。也有高级运算符,如sumcount,summaryStatistics等,它们建立在reduceand之上collect

// calculating sum using reduce terminal operator
people.stream()
    .mapToInt(Person::getAge)
    .reduce(0, (total, currentValue) -> total + currentValue);// calculating sum using sum terminal operator
people.stream()
    .mapToInt(Person::getAge)
    .sum();// calculating count using count terminal operator
people.stream()
    .mapToInt(Person::getAge)
    .count();// calculating summary
IntSummaryStatistics ageStatistics = people.stream()
    .mapToInt(Person::getAge)
    .summaryStatistics();
ageStatistics.getAverage();
ageStatistics.getCount();
ageStatistics.getMax();
ageStatistics.getMin();
ageStatistics.getSum();