开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第8天,点击查看活动详情
0.背景
HR急匆匆跑到荡平宇宙软件开发部,大声喊:“发礼物了,挑两个人去下面搬礼物。”
大家都很开心,现在坐在办公室里,有两个小组,A小组有小黄、小孙、老罗,B小组有小刘、小孟。
老罗问小黄:“不是需要两个人吗?每个组挑一个人的话,有多少种情况呢?”
小黄说:“那还不简单,我给你写一下。正好是六种情况。”
1.组合如何用代码表达呢?
老罗接着问小黄:“你看啊,现在只有这么点儿人,如果人多了呢?你能写一个程序来表达这个逻辑吗?然后之后再加一个随机数,这样就不用每次还讨论谁去了。”
小黄挠挠头,头发干枯分叉还多油。她写到:
public class PersonPairTest {
public static class Person {
public static final Person[] aGroupPersonList = {new Person("小黄"), new Person("小孙"), new Person("老罗")};
public static final Person[] bGroupPersonList = {new Person("小刘"), new Person("小孟")};
private String name;
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
'}';
}
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
public static void main(String[] args) {
for (Person personInA : Person.aGroupPersonList) {
for (Person personInB : Person.bGroupPersonList) {
System.out.println("(" + personInA.getName() + "," + personInB.getName() + ")");
}
}
}
}
2.换成函数式的思维呢?
老罗微微一笑:“你看你这个双层for循环,如果变动了需求,是不是就要写注释、封装函数,一会儿里层加代码,一会儿外层加代码了?”
小黄尴尬地说:“我只会这样,我听说过流,可以用流处理吗?但是一会儿是数组,一会儿是数组中的元素,一会儿又组合成数组了,我不会写这种的。”
老罗不慌不忙地敲到:
public class PersonPairTest {
public static class Person {
public static final Person[] aGroupPersonList = {new Person("小黄"), new Person("小孙"), new Person("老罗")};
public static final Person[] bGroupPersonList = {new Person("小刘"), new Person("小孟")};
private String name;
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
'}';
}
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
public static void main(String[] args) {
for (Person personInA : Person.aGroupPersonList) {
for (Person personInB : Person.bGroupPersonList) {
System.out.println("(" + personInA.getName() + "," + personInB.getName() + ")");
}
}
System.out.println("-----------------------------------------");
Stream<Person> aStream = Arrays.stream(Person.aGroupPersonList);
List<String[]> collect = aStream.flatMap(
personInA -> Arrays.stream(Person.bGroupPersonList).map(
personInB -> new String[]{personInA.getName(), personInB.getName()}
)
)
.collect(Collectors.toList());
for (String[] strings : collect) {
StringJoiner stringJoiner = new StringJoiner(",", "(", ")");
for (String string : strings) {
stringJoiner.add(string);
}
System.out.println(stringJoiner.toString());
}
}
}
小黄有一点疑惑:“为什么有个我不认识的 flatMap 它是做什么的呢?”
老罗很欣慰地说:“你发现了重点,如果我们不用这个函数,我添加一些注释,我们看一下处理的过程”
List<List<String[]>> pairListList = aStream.map(personInA -> {
// 每个A组的人
List<String[]> pairList = Arrays.stream(Person.bGroupPersonList).map(
// 都能和B组的人组成组合
personInB -> new String[]{personInA.getName(), personInB.getName()}
).collect(Collectors.toList());
// 然后就返回了一个 List
return pairList;
}).collect(Collectors.toList());
如此一来啊,我们其实获取到了结果,但是结果也是分组的结果,就像下面这样:
而我们,希望它是平的,就像下面这样:
咋办呢?我们就需要 flatMap ,看好了啊,我把 map 那一步的代码也附上,看看这一步做了什么。
List<List<String[]>> pairListList = aStream.map(personInA -> {
// 每个A组的人
List<String[]> pairList = Arrays.stream(Person.bGroupPersonList).map(
// 都能和B组的人组成组合
personInB -> new String[]{personInA.getName(), personInB.getName()}
).collect(Collectors.toList());
// 然后就返回了一个 List
return pairList;
}).collect(Collectors.toList());
// 从这里开始,进行推平。
List<String[]> pairList = pairListList.stream().flatMap(
(List<String[]> complicatedList) -> {
// 把每个List的流 推平了,把List里的元素连接在一起。
return complicatedList.stream();
}).collect(Collectors.toList());
推平这个流的句子,可以直接写成:
// 把每个List的流 推平了 ,连接在一起。
List<String[]> pairList = pairListList.stream()
.flatMap(Collection::stream).collect(Collectors.toList());
3. flatMap 需要一个生成 Stream 的 Function
小黄继续问道:“为啥传入的是一个流呢?”
老罗不慌不忙地打开源码部分给她看:
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);
这里的形参比较难看懂,我们拆分一下:
| 代码 | 含义 |
|---|---|
<R> Stream<R> | 泛型返回值,更好适配 |
flatMap | |
Function | 函数接口,接收一个对象,返回一个对象 |
? super T | T的超类(包括T自身),一般来说是传入T类型的对象 |
? extends Stream<? extends R> | <? extends R> R或者R的子类,然后又在前面拓展这个流的适应能力,流的子类也可以 |
mapper | 参数名称,告诉用户是在进行映射 |
这里不太适应的地方,就在于 Function 里类型上界和类型下界的运用。
4.【拓展】类型上界与类型下界
这一部分需要自己思考一下,尤其是 <? super T> 的部分,下界通配符给出一个下界,但是它的子类也属于它,所以也可以加进去的。
由于这个文章写得很棒,所以我不打算再写一篇了。为了防止链接丢失,我把最核心的内容放在下面:
PECS(Producer Extends Consumer Super)原则:
- 频繁往外读取内容的,适合用上界Extends。这里的读取,指的是对List之类的容器而言。
- 经常往里插入的,适合用下界Super。这里的插入,指的是对List之类的容器而言。
由于这里经常有人问PECS该如何理解,我把Stack Overflow上的经典帖子也附上: What is PECS (Producer Extends Consumer Super)?
它的主要内容如下:
"PECS" is from the collection's point of view. If you are only pulling items from a generic collection, it is a producer and you should use
extends; if you are only stuffing items in, it is a consumer and you should usesuper. If you do both with the same collection, you shouldn't use eitherextendsorsuper.
Case 1: You want to go through the collection and do things with each item.
Then the list is a producer, so you should use aCollection<? extends Thing>
Case 2: You want to add things to the collection.
Then the list is a consumer, so you should use aCollection<? super Thing>.
老罗正讲着……突然,HR叫她:“老罗!你的专属生日会,快来一趟。” 老罗一怔,五味杂陈,她对着小黄说:“要努力学习啊,公司的未来就靠你们年轻人了。”
说着,老罗戴上耳机,一首《Рiчка》伴随她,踏上新的征程。
结语
本例中,我们介绍了流处理中的 flatMap 的部分,它在我们使用 Stream 常常遇到的困难(得到了几段答案,要拼在一起)的解决方法。
你还想到了什么呢?欢迎你在评论区和其他读者讨论!