Java系列:Flatmap
#java #功能 #data

Java 8是迈向现代编程语言的伟大一步。此版本中添加的关键功能之一是Java流。它为数据处理提供了许多方便的操作。其中之一是一个flatMap(),非常广泛地用于解开并将多个集合合并为一个。

本文是“ Java系列”的一部分,该系列涵盖了标准和流行库中有用的Java功能。有关此内容的更多帖子,可以在此处在Dev.to或我的home page上找到。

问题陈述

很多次使用Java代码时,我们最终得到了以下一个普通的旧Java对象(PO​​JO):

public record Parent(List<Child> childs) {}

他们可以代表数据库实体或数据传输对象(DTO)。通常,它们用于构建数据。假设我们获得了Parent对象的列表,但是我们想在所有Child的列表中操作,这些ChildParent的一部分。我们如何从所有Parent对象中提取Child对象并将它们组合到单个列表中?天真的方法是使用一个循环:

List<Child> children = new ArrayList<>();

for (Parent parent: parents) {
    List<Child> bs = parent.childs();
    children.addAll(bs);
}

,但看起来不太干净。相反,我们可以使用Java流:

List<Child> children = new ArrayList<>();

parents.stream()
    .map(parent -> parent.Childs())
    .forEach(list -> children.addAll(list));

,但它也有缺点。假设,一旦我们获得了所有Child对象的列表,我们想对它们进行修改,汇总或对它们进行计算。理想情况下,最好在同一流中执行这些动作。不幸的是,事实并非如此,上面示例中的forEach()方法正在结束流处理,这使得无法在同一流中处理Child记录。

解决方案

幸运的是Java的创建者预见了这个问题,并引入了flatMap()函数,该功能是java.util.stream.Stream类的一部分。

这个想法非常简单。它在流的每个元素中做两件事:

  • 地图 - 将一个元素从流转换为新流,因此我们将获得流的流,
  • 平坦 - 先前操作的结果合并为一个流。

可视化它考虑以下情况:

Map function

假设我们有一个Author对象流,该对象具有称为books()的方法,该方法返回Book对象列表。现在,假设我们希望访问所有作者写的所有书籍,以对其进行进一步的操作。如果我们使用map()函数,在该函数中我们称为books()方法,我们将获得Book对象列表的流。这不是我们想要的。

我们想拥有的是Book对象的流,而不是其列表的流。我们如何克服它?而是使用flatmap()

Flatmap function

与以前一样,我们需要调用Author类的books()方法以获取Books的列表。唯一的区别是,需要将输入转换为由Java流表示的多个值。这是flatMap()方法的要求。我们在其中进行的任何操作都需要返回Stream<T>对象。

可以用代码反映相同的情况:

List<List<Book>> listsOfListOfBooks = authors.stream()
    .map(Author::books)
    .toList();

List<Book> listOfBooks = authors.stream()
    .flatMap(author -> author.books().stream())
    .toList();

要翻译Book对象的列表,使用了标准Collection.stream()方法。

什么时候使用?

Un -Innrap&Operate

flatMap()方法可能方便的情况下,最常见的情况是,其中一个流操作产生了一系列对象,我们想对每个对象做出进一步的操作。

可视化它,让我们回到使用BookAuthor记录的上一个示例。这是他们的定义:

public record Book(String title) {}
public record Author(String name, List<Book> books) {}

现在,假设我们要创建一种将Author对象列表的方法作为输入,并制作了这些作者写的所有书名的列表:

List<String> getAllBookTitles(List<Author> authors) {
return authors.stream()
    .flatMap(author -> author.books().stream())
    .map(Book::title)
    .toList();
}

Author对象列表转换为流列表后,首先使用flatMap()操作。调用books()方法以获取其列表,然后将其更改为流。然后,flatMap()将所有结果流合并到一个流中,因此可以调用title()以获取书名标题的字符串表示形式。最后,将流中每个元素的结果收集到列表中。

上述方法可以更好地编写。我们可以将调用books()stream()方法分别分别为两个操作-map()flatMap()-以获取一个漂亮的代码:

List<String> getAllBookTitles(List<Author> authors) {
return authors.stream()
    .map(Author::books)
    .flatMap(List::stream)
    .map(Book::title)
    .toList();
}

合并列表

flatMap()非常有用的另一种情况是,当我们想结合两个或更多列表(或任何其他java.util.Collection)时。

List<String> mergeLists(List<String> left, List<String> right) {
return Stream.of(left, right)
    .flatMap(List::stream)
    .toList();
}

这种方法的一个很大的优势是,在flatMap()之后,我们不需要立即关闭流。相反,我们可以在每个对象上应用其他操作,例如过滤,映射,聚合等。

更清洁,更高效。

从嵌套的可选中获取价值

除了Java Streams flatMap()方法外,可以在Optional对象上调用。它用于解开嵌套在另一个Optional中的Optional

假设我们有以下记录:

public record Address(String street, String buildingNo, Optional<String> apartmentNo) {}

现在假设我们将拥有一个Optional<Address>,并希望提取apartmentNo的值。没有flatMap()的代码看起来像这样:

String extractApartmentNo(Optional<Address> address) {
    if (address.isEmpty()) {
        return "";
    }

    return address.get().apartmentNo().orElse("");
}

第一步是从Optional解开值,这可能是空的。只有在检查它之后,我们才能进行公寓地址(并处理空价值)。

这种方法还可以,但是可以使用flatMap()
做得更好

String extractApartmentNo(Optional<Address> address) {
return address
        .flatMap(Address::apartmentNo)
        .orElse("");
}

这种方法比前一个方法更好。两个选择 - 父和子女 - 是否在单个表达式中持有null值。

概括

将流引入Java使数据处理更加容易。它为我们带来了很多方便的操作。 flatMap()是其中之一,它使我们有可能将多个流合并为一个或将嵌套的流变成一个。这是一个非常普遍的模式,在实际项目中多次使用。

这篇文章中的代码和测试可以在我的存储库中找到:wkrzywiec/java-series | Github

参考


最初于2021年12月12日在https://wkrzywiec.is-a.dev出版。