Java Stream API实用指南


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

在使用命令式编程时,开发人员使用语言结构来编写 要执行的操作和操作方法。在使用声明性编程时,开发人员必须只关注定义要做什么,语言或框架负责如何做部分。因此,在声明性编程中,代码简洁且不易出错。

通常在集合上执行的操作可以分类如下。虽然下面的列表并不详尽,但它涵盖了我们在日常编程中遇到的大多数用例。我将在示例中使用下面提到的操作来介绍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 = ...
List<String> namesOfPeopleBelow20 = people.stream()  // bulding a stream
    .filter(person -> person.getAge() < 20)  
// pipelining a computation
    .map(Person::getName)  
// pipelining another computation
    .collect(Collectors.toList());  
// terminating a stream

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

  • 流构建器 - 在上面的示例中,我们有一个由Person表示的集合people。在Collection接口上添加方法来构建Java 8中stream() 的流。除了Collection是流的常见源之外,还有数组(Arrays.stream())和生成器函数(Stream.iterate() 和Stream.generate())。
  • 中间操作 - 创建流对象后,您可以通过链接操作在流上应用零或一个或多个操作,就像在构建器模式中一样。所有你在上面的例子中看到的方法:filter和map,它们被称为中间操作。
  • 终端操作 - 一旦应用了所有计算,您就可以通过应用强制终端运算符来完成管道。终端操作符也是Stream接口上的方法,返回的不是Stream的结果类型。在上面的示例中,collect(Collectors.toList())返回一个实例List。结果类型可以是任意集合。

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

转换
转换意味着转换存储在集合的每个元素中的值的类型。假设我们想要从人物集合中获取人物名称的集合。在这种情况下,我们必须使用转换操作将人转换为名称。
在下面的示例中,我们使用map这个中间运算符转换People为人的名称,人的名称是String类型。Person::getName是一个方法引用,它等同于 person -> person.getName()并且是Function的一个实例。

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


过滤
正如过滤这个文字所暗示的那样,只有当对象满足谓词所规定的条件时,过滤操作才允许对象流过自身。过滤器运算符由Predicate它应用于Stream之前组成。
过滤也可以被认为是基于计数选择少量元素。流API提供skip()和limit()运营商用于这一目的。
在下面的第一个示例中,person -> person.getAge() < 20谓词用于构建仅包含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());


搜索
搜索集合意味着基于标准搜索元素或元素的存在,该标准可再次表示为一个Predicate。搜索元素可能会也可能不会返回值,因此您将获得一个Optional. 。搜索元素的存在将返回a boolean。
在下面的示例中,使用搜索元素并使用findAny()搜索存在来完成搜索anyMatch()。

// searching for a element
Optional<Person> any = people.stream()
    .filter(person -> person.getAge() < 20)
    .findAny();

// searching for existence
boolean isAnyOneInGroupLessThan20Years = people.stream()
    .anyMatch(person -> person.getAge() < 20);


重新排序
如果要对集合中的元素进行排序,可以使用sorted中间运算符。它需要一个Comparator接口的实例。为了创建实例,我使用了带有comparing工厂方法的Comparator。
在下面的示例中,生成的集合按年龄降序排序。

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

与我们迄今为止看到的其他操作不同,sorted操作是有状态的。这意味着操作员必须在排序结果可以提供给进一步的中间或终端操作之前查看流中的所有元素。另一个例子是distinct

统计合并
有时您想从集合中获取信息。例如,推导出所有人的年龄总和。在StreamAPI中,这是使用终端操作符实现的。reduce和collect都是为此目的提供的通用终端操作。高水平的人也很喜欢sum,count,summaryStatistics,等,它们在建立reduce和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();


reduce并且collect是 Reduction合并操作。reduce用于不可变的合并,collect而是用于可变的合并。不可逆的合并是首选方法,这对于性能很重要。

分组
分组也可以称为分类。有时我们想要将一个集合分成几个组。在这种情况下得到的数据结构是Map键表示分组因子,值表示特定组的特征。StreamAPI提供Collectors.groupingBy用于这种情形。

在下面的所有示例中,分组是使用性别完成的。不同之处在于值。
在第一个示例中,Person为每个组创建了一个集合。在第二个中,Collectors.mapping()用于提取每个名称Person以创建名称集合。在第三个中,Person提取每个的年龄并计算每个组的平均年龄。

// Grouping people by gender
Map<Gender, List<Person>> peopleByGender = people.stream()
    .collect(Collectors.groupingBy(
        Person::getGender,
        Collectors.toList()));

// Grouping person names by gender
Map<Gender, List<String>> nameByGender = people.stream()
    .collect(Collectors.groupingBy(
        Person::getGender,
        Collectors.mapping(Person::getName, Collectors.toList())));

// Grouping average age by gender
Map<Gender, Double> averageAgeByGender = people.stream()
    .collect(Collectors.groupingBy(
        Person::getGender, 
        Collectors.averagingInt(Person::getAge)
    ));

总结
在本指南中,我们看到Java StreamAPI提供了许多内置功能,以帮助使用流管道对集合执行操作。API是声明性的,它使代码精确且不易出错。我希望本指南为您提供足够的信息,以便您Stream在日常编程中有效地使用Java API。