Java 8是迈向现代编程语言的伟大一步。此版本中添加的关键功能之一是Java流。它为数据处理提供了许多方便的操作。其中之一是一个flatMap()
,非常广泛地用于解开并将多个集合合并为一个。
本文是“ Java系列”的一部分,该系列涵盖了标准和流行库中有用的Java功能。有关此内容的更多帖子,可以在此处在Dev.to或我的home page上找到。
问题陈述
很多次使用Java代码时,我们最终得到了以下一个普通的旧Java对象(POJO):
public record Parent(List<Child> childs) {}
他们可以代表数据库实体或数据传输对象(DTO)。通常,它们用于构建数据。假设我们获得了Parent
对象的列表,但是我们想在所有Child
的列表中操作,这些Child
是Parent
的一部分。我们如何从所有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
类的一部分。
这个想法非常简单。它在流的每个元素中做两件事:
- 地图 - 将一个元素从流转换为新流,因此我们将获得流的流,
- 平坦 - 先前操作的结果合并为一个流。
可视化它考虑以下情况:
假设我们有一个Author
对象流,该对象具有称为books()
的方法,该方法返回Book
对象列表。现在,假设我们希望访问所有作者写的所有书籍,以对其进行进一步的操作。如果我们使用map()
函数,在该函数中我们称为books()
方法,我们将获得Book
对象列表的流。这不是我们想要的。
我们想拥有的是Book
对象的流,而不是其列表的流。我们如何克服它?而是使用flatmap()
:
与以前一样,我们需要调用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()
方法可能方便的情况下,最常见的情况是,其中一个流操作产生了一系列对象,我们想对每个对象做出进一步的操作。
可视化它,让我们回到使用Book
和Author
记录的上一个示例。这是他们的定义:
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
参考
- Part 2: Processing Data with Java SE 8 Streams | Oracle.com
- Interface Stream | Docs Oracle.com
- Tired of Null Pointer Exceptions? Consider Using Java SE 8's "Optional"! | Oracle.com
最初于2021年12月12日在https://wkrzywiec.is-a.dev出版。