作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO
联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬
Stream API与接口默认方法、静态方法
之前学习函数式接口时,曾经提到过Java8新出的接口静态方法、默认方法:
为什么Java8要引入静态方法和默认方法呢?
原因可能有多种,但其中一种肯定是为了“静默升级”(我自己造的一个词)。打开Collection接口:
我们发现有3个default方法,而且都是JDK1.8新增的:
这下明白了吧,list.stream().filter()...用的这么爽,其实都直接继承自顶级父接口Collection。
这和引入default有啥关系呢?
试想一下,要想操作Stream必须先获取Stream,获取流的方法理应放在Collection及其子接口、实现类中。但如果作为抽象方法抽取到Collection中,那么原先的整个继承链都会产生较大的震动:
JDK官方要从Collection接口沿着继承链向下都实现一遍stream()方法。这还不是最大的问题,最致命的是全球各地保不齐就有人直接实现了Collection,比如MyArrayList啥的,此时如果贸然往Collection增加一个抽象方法,那么当他们升级到JDK1.8后就会立即编译错误,强制他们自己实现stream()...
到这,大家应该恍然大悟了:
害...什么静默升级,不就是向前兼容嘛!
所以JDK的做法是,把获取Stream的一部分方法封装到StreamSupport类,另一部分封装到Stream类,StreamSupport用来补足原先的集合体系,比如Collection,然后引入default方法包装一下,内部调用StreamSupport完成偷天换日。而得到Stream后的一系列filter、map操作是针对Stream的,已经封装在Stream类中,和原来的集合无关。
所以,StreamSupport+default就是原先集合体系和Stream之间的“中介”:
调用者-->Collection-->StreamSupport+default-->Stream
外部调用者还以为集合体系新增了Stream API呢。
接口静态方法也有很多使用场景,大家会在后续学习中看到:
总之,引入接口默认方法和静态方法后,接口越来越像一个类。从某个角度来说,这种做法破坏了Java的单继承原则。Java原本的特点是“单继承、多实现”,假设接口A和接口B都有methodTest(),那么class Test implements interfaceA, interfaceB时,就不得不考虑使用哪个父接口的methodTest()方法。JDK的做法是,编译时强制子类覆盖父接口同名的方法。
Steam API
我们最常用的集合其实来自两个派系:Collection和Map,实际开发时使用率大概是这样的:
- ArrayList:50%
- HashMap:40%
- 其他:10%
而用到Stream的地方,占比就更极端了:
- List:90%
- Set:5%
- Map:5%
对于一般人来说,只要学好List的Stream用法即可。
认识几个重要的接口与类
Stream的方法众多,不要期望能一次性融会贯通,一定要先了解整个API框架。有几个很重要的接口和类:
- Collection
- Stream
- StreamSupport
- Collector
- Collectors
Collection
之前介绍过了,为了不影响之前的实现,JDK引入了接口默认方法,并且在Collection中提供了一系列将集合转为Stream的方法:
要想使用Stream API,第一步就是获取Stream,而Collection提供了stream()和parallelStream()两个方法,后续Collection的子类比如ArrayList、HashSet等都可以直接使用顶级父接口定义好的默认方法将自身集合转为Stream。
Collection:定义stream()/parallelStream()
|--List
|--ArrayList:list.stream().filter().map().collect(...)
|--LinkedList:list.stream().filter().map().collect(...)
|--Vector
|--Set
|--HashSet
|--TreeSet
Stream
Java的集合在设计之初就只是一种容器,用来存储元素,内部并没有提供处理元素的方法。更多时候,我们其实是使用集合提供的遍历方法,然后手动在外部进行判断并处理元素。
Stream是什么呢?简单来说,可以理解为更高级的Iterator,把集合转为Stream后,我们就可以使用Stream对元素进行一系列操作。
来,感受一下,平时使用的filter()、map()、sorted()、collect()都来自哪:
方法太多了,记忆时需要带点技巧,比如分类记忆。
常用的有两种分类方法:
- 终止/中间操作
- 短路/非短路操作
前一种分类和我们关系大一些,所以我们按终止、中间操作分类记忆。
所谓中间操作和终止操作,可以粗略地理解为后面还能不能跟其他的方法。比如filter后面还可以跟map等操作,那么filter就是中间操作,而collect后返回的就是元素了,而不是流,无法继续使用。就好比一缕山泉,经过小草、小花、竹林,还是一缕水,但到了你的锅里煮成一碗粥,就没法继续使用了。
还有几个不是特别常用的操作就不放在思维导图里了,这里简要介绍一下。比如,除了Collection接口中定义的stream()和parallelStream(),Stream也定义了创建流的方法(不常用):
还有一个合并流的方法(知道即可):
StreamSupport
没啥好介绍的,一般不会直接使用StreamSupport,Collection接口借助它实现了stream()和parallelStream()。
collect()、Collector、Collectors
我们先不说这几个是什么、有什么关系,直接通过代码演示哪里会用到:
public class StreamTest {
private static List<Person> list;
static {
list = new ArrayList<>();
list.add(new Person("甲", 18, "杭州", 999.9));
list.add(new Person("乙", 19, "温州", 777.7));
list.add(new Person("丙", 21, "杭州", 888.8));
list.add(new Person("丁", 17, "宁波", 888.8));
}
public static void main(String[] args) {
List<Person> result = list.stream()
.filter(person -> person.getAge() > 20)
.collect(Collectors.toList());
}
@Data
@AllArgsConstructor
@NoArgsConstructor
static class Person {
private String name;
private Integer age;
private String address;
private Double salary;
}
}
collect()是用来收集处理后的元素的,它有两个重载的方法:
我们暂时只看下面那个,它接收一个Collector对象,而我们一般不会自己去new Collector对象,因为JDK给我提供了Collectors,可以调用Collectors提供的方法返回Collector对象:
所以collect()、Collector、Collectors三者的关系是:
collect()通过传入不同的Collector对象来明确如何收集元素,比如收集成List还是Set还是拼接字符串?而通常我们不需要自己实现Collector接口,只需要通过Collectors获取。
这倒颇有点像Executor和Executors的关系,一个是线程池接口,一个是线程池工具类。
如何高效学习Stream API
和几个重要接口、类混个脸熟后,我们来谈谈如何高效学习Stream API。很多同学应该已经被上面的内容吓到了,不要怕,通过后面的实操,整个知识脉络很快就会清晰起来。但还是那句话,一开始不要扣细节,先抓主干:
初学者如果对stream的流式操作感到陌生,可以暂时理解为外部迭代(实际不是这样的,后面会大家一起观察):
特别注意reduce():
特别适合做累加、累乘啥的。
铺垫结束,接下来我们通过实战来正式学习Stream API。学习其他技术都可以追求理论深度,唯独Stream API就是一个字:干!
基础操作
map/filter
先来个最简单:
public class StreamTest {
private static List<Person> list;
static {
list = new ArrayList<>();
list.add(new Person("i", 18, "杭州", 999.9));
list.add(new Person("am", 19, "温州", 777.7));
list.add(new Person("iron", 21, "杭州", 888.8));
list.add(new Person("man", 17, "宁波", 888.8));
}
public static void main(String[] args) {
// 我们先学中间操作
// 1.先获取流(不用管其他乱七八糟的创建方式,记住这一个就能应付95%的场景)
Stream<Person> stream = list.stream();
// 2.过滤得到年纪大于18岁的(filter表示过滤【得到】符合传入条件的元素,而不是过滤【去除】)
Stream<Person> filteredByAgeStream = stream.filter(person -> person.getAge() > 18);
// 3.只要名字,不需要整个Person对象(为什么在这个案例中,filter只能用Lambda,map却可以用方法引用?)
Stream<String> nameStream = filteredByAgeStream.map(Person::getName);
// 4.现在返回值是Stream<String>,没法直接使用,帮我收集成List<String>
List<String> nameList = nameStream.collect(Collectors.toList());
// 现在还对collect()为什么传递Collectors.toList()感到懵逼吗?
}
@Data
@AllArgsConstructor
@NoArgsConstructor
static class Person {
private String name;
private Integer age;
private String address;
private Double salary;
}
}
再来一个:
public static void main(String[] args) {
// 直接链式操作
List<String> nameList = list.stream()
.filter(person -> person.getAge() > 18)
.map(Person::getName)
.collect(Collectors.toList());
}
sorted
试着加入sorted()玩一下。
在此之前,我们先来见见一位老朋友:Comparator。这个接口其实早在JDK1.2就有了,但当时只有两个方法:
- compare()
- equals()
JDK1.8通过默认方法的形式引入了很多额外的方法,比如reversed()、Comparing()等。
public class StreamTest {
private static List<Person> list;
static {
list = new ArrayList<>();
list.add(new Person("i", 18, "杭州", 999.9));
list.add(new Person("am", 19, "温州", 777.7));
list.add(new Person("iron", 21, "杭州", 888.8));
list.add(new Person("man", 17, "宁波", 888.8));
}
public static void main(String[] args) {
// JDK8之前:Collections工具类+匿名内部类。Collections类似于Arrays工具类,我经常用Arrays.asList()
Collections.sort(list, new Comparator<Person>() {
@Override
public int compare(Person p1, Person p2) {
return p1.getName().length()-p2.getName().length();
}
});
// JDK8之前:List本身也实现了sort()
list.sort(new Comparator<Person>() {
@Override
public int compare(Person p1, Person p2) {
return p1.getName().length()-p2.getName().length();
}
});
// JDK8之后:Lambda传参给Comparator接口,其实就是实现Comparator#compare()。注意,equals()是Object的,不妨碍
list.sort((p1,p2)->p1.getName().length()-p2.getName().length());
// JDK8之后:使用JDK1.8为Comparator接口新增的comparing()方法
list.sort(Comparator.comparingInt(p -> p.getName().length()));
}
@Data
@AllArgsConstructor
@NoArgsConstructor
static class Person {
private String name;
private Integer age;
private String address;
private Double salary;
}
}
大家不好奇吗?sort()需要的是Comparator接口的实现,调用Comparator.comparing()怎么也可以?
好家伙,Comparator.comparing()返回的也是Comparator...
OK,铺垫够了,来玩一下Stream#sorted(),看看和List#sort()有啥区别。
public class StreamTest {
private static List<Person> list;
static {
list = new ArrayList<>();
list.add(new Person("i", 18, "杭州", 999.9));
list.add(new Person("am", 19, "温州", 777.7));
list.add(new Person("iron", 21, "杭州", 888.8));
list.add(new Person("man", 17, "宁波", 888.8));
}
public static void main(String[] args) {
// 直接链式操作
List<String> nameList = list.stream()
.filter(person -> person.getAge() > 18)
.map(Person::getName)
.collect(Collectors.toList());
System.out.println(nameList);
// 我想按姓名长度排序
List<String> sortedNameList = list.stream()
.filter(person -> person.getAge() > 18)
.map(Person::getName)
.sorted()
.collect(Collectors.toList());
System.out.println(sortedNameList);
// 你:我擦,说好的排序呢?
// Stream:别扯淡,你告诉我排序规则了吗?(默认自然排序)
// 明白了,那就按照长度倒序吧(注意细节啊,str2-str1才是倒序)
List<String> realSortedNameList = list.stream()
.filter(person -> person.getAge() > 18)
.map(Person::getName)
.sorted((str1, str2) -> str2.length() - str1.length())
.collect(Collectors.toList());
System.out.println(realSortedNameList);
// 优化一下:我记得在之前那张很大的思维导图上看到过,sorted()有重载方法,是sorted(Comparator)
// 上面Lambda其实就是调用sorted(Comparator),用Lambda给Comparator接口赋值
// 但Comparator还供了一些方法,能返回Comparator实例
List<String> optimizeNameList = list.stream()
.filter(person -> person.getAge() > 18)
.map(Person::getName)
.sorted(Comparator.reverseOrder())
.collect(Collectors.toList());
System.out.println(optimizeNameList);
// 又是一样的套路,Comparator.reverseOrder()返回的其实是一个Comparator!!
// 但上面的有点投机取巧,来个正常点的,使用Comparator.comparing()
List<String> result1 = list.stream()
.filter(person -> person.getAge() > 18)
.map(Person::getName)
.sorted(Comparator.comparing(t -> t, (str1, str2) -> str2.length() - str1.length()))
.collect(Collectors.toList());
System.out.println(result1);
// 我去,更麻烦了!!
// 不急,我们先来了解上面案例中Comparator的两个参数
// 第一个是Function映射,就是指定要排序的字段,由于经过上一步map操作,已经是name了,就不需要映射了,所以是t->t
// 第二个是比较规则
// 我们把map和sorted调换一下顺序,看起来就不那么别扭了
List<String> result2 = list.stream()
.filter(person -> person.getAge() > 18)
.sorted(Comparator.comparing(Person::getName, String::compareTo).reversed())
.map(Person::getName)
.collect(Collectors.toList());
System.out.println(result2);
// 为什么Comparator.comparing().reversed()可以链式调用呢?
// 上面说了哦,因为Comparator.comparing()返回的还是Comparator对象~
}
@Data
@AllArgsConstructor
@NoArgsConstructor
static class Person {
private String name;
private Integer age;
private String address;
private Double salary;
}
}
limit/skip
public class StreamTest {
private static List<Person> list;
static {
list = new ArrayList<>();
list.add(new Person("i", 18, "杭州", 999.9));
list.add(new Person("am", 19, "温州", 777.7));
list.add(new Person("iron", 21, "杭州", 888.8));
list.add(new Person("man", 17, "宁波", 888.8));
}
public static void main(String[] args) {
List<String> result = list.stream()
.filter(person -> person.getAge() > 17)
// peek()先不用管,它不会影响整个流程,就是打印看看filter操作后还剩什么元素
.peek(person -> System.out.println(person.getName()))
.skip(1)
.limit(2)
.map(Person::getName)
.collect(Collectors.toList());
System.out.println(result);
}
@Data
@AllArgsConstructor
@NoArgsConstructor
static class Person {
private String name;
private Integer age;
private String address;
private Double salary;
}
}
结果
==== 过滤后的元素有3个 ====
i
am
iron
==== skip(1)+limit(2)后的元素 ====
[am, iron]
所谓的skip(N)就是跳过前面N个元素,limit(N)就是只取N个元素。
collect
collect()是最重要、最难掌握、同时也是功能最丰富的方法。
最常用的4个方法:Collectors.toList()、Collectors.toSet()、Collectors.toMap()、Collectors.joining()
public class StreamTest {
private static List<Person> list;
static {
list = new ArrayList<>();
list.add(new Person("i", 18, "杭州", 999.9));
list.add(new Person("am", 19, "温州", 777.7));
list.add(new Person("iron", 21, "杭州", 888.8));
list.add(new Person("man", 17, "宁波", 888.8));
}
public static void main(String[] args) {
// 最常用的4个方法
// 把结果收集为List
List<String> toList = list.stream().map(Person::getAddress).collect(Collectors.toList());
System.out.println(toList);
// 把结果收集为Set
Set<String> toSet = list.stream().map(Person::getAddress).collect(Collectors.toSet());
System.out.println(toSet);
// 把结果收集为Map,前面的是key,后面的是value,如果你希望value是具体的某个字段,可以改为toMap(Person::getName, person -> person.getAge())
Map<String, Person> nameToPersonMap = list.stream().collect(Collectors.toMap(Person::getName, person -> person));
System.out.println(nameToPersonMap);
// 把结果收集起来,并用指定分隔符拼接
String result = list.stream().map(Person::getAddress).collect(Collectors.joining("~"));
System.out.println(result);
}
@Data
@AllArgsConstructor
@NoArgsConstructor
static class Person {
private String name;
private Integer age;
private String address;
private Double salary;
}
}
关于collect收集成Map的操作,有一个小坑需要注意:
public class StreamTest {
private static List<Person> list;
static {
list = new ArrayList<>();
list.add(new Person("i", 18, "杭州", 999.9));
list.add(new Person("am", 19, "温州", 777.7));
list.add(new Person("iron", 21, "杭州", 888.8));
list.add(new Person("iron", 17, "宁波", 888.8));
}
public static void main(String[] args) {
Map<String, Person> nameToPersonMap = list.stream().collect(Collectors.toMap(Person::getName, person -> person));
System.out.println(nameToPersonMap);
}
@Getter
@Setter
@AllArgsConstructor
static class Person {
private String name;
private Integer age;
private String address;
private Double salary;
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
", address='" + address + '\'' +
", salary=" + salary +
'}';
}
}
}
尝试运行上面的代码,会观察到如下异常:
Exception in thread "main" java.lang.IllegalStateException: Duplicate key Person{name='iron', age=21, address='杭州', salary=888.8}
这是因为toMap()不允许key重复,我们必须指定key冲突时的解决策略(比如,保留已存在的key):
public static void main(String[] args) {
Map<String, Person> nameToPersonMap = list.stream()
.collect(Collectors.toMap(Person::getName, person -> person, (preKey, nextKey) -> preKey));
System.out.println(nameToPersonMap);
}
如果你希望key覆盖,可以把(preKey, nextKey) -> preKey)换成(preKey, nextKey) -> nextKey)。
你可能会在同事的代码中发现另一种写法:
public static void main(String[] args) {
Map<String, Person> nameToPersonMap = list.stream().collect(Collectors.toMap(Person::getName, Function.identity());
System.out.println(nameToPersonMap);
}
Function.identity()其实就是v->v:
但它依然没有解决key冲突的问题,而且对于大部分人来说,相比person->person,Function.identity()的可读性不佳。
聚合:max/min/count
max/min
public class StreamTest {
private static List<Person> list;
static {
list = new ArrayList<>();
list.add(new Person("i", 18, "杭州", 999.9));
list.add(new Person("am", 19, "温州", 777.7));
list.add(new Person("iron", 21, "杭州", 888.8));
list.add(new Person("man", 17, "宁波", 888.8));
}
public static void main(String[] args) {
// 匿名内部类的方式,实现Comparator,明确按什么规则比较(所谓最大,必然是在某种规则下的最值)
Optional<Integer> maxAge = list.stream().map(Person::getAge).max(new Comparator<Integer>() {
@Override
public int compare(Integer age1, Integer age2) {
return age1 - age2;
}
});
System.out.println(maxAge.orElse(0));
Optional<Integer> max = list.stream().map(Person::getAge).max(Integer::compareTo);
System.out.println(max.orElse(0));
}
@Data
@AllArgsConstructor
@NoArgsConstructor
static class Person {
private String name;
private Integer age;
private String address;
private Double salary;
}
}
count
public static void main(String[] args) {
long count = list.stream().filter(person -> person.getAge() > 18).count();
System.out.println(count);
}
去重:distinct
public class StreamTest {
private static List<Person> list;
static {
list = new ArrayList<>();
list.add(new Person("i", 18, "杭州", 999.9));
list.add(new Person("am", 19, "温州", 777.7));
list.add(new Person("iron", 21, "杭州", 888.8));
list.add(new Person("man", 17, "宁波", 888.8));
}
public static void main(String[] args) {
long count = list.stream().map(Person::getAddress).distinct().count();
System.out.println(count);
}
@Data
@AllArgsConstructor
@NoArgsConstructor
static class Person {
private String name;
private Integer age;
private String address;
private Double salary;
}
}
所谓“去重”,就要明确怎样才算“重复”。那么,distinct()是基于什么标准呢?
还是那两样:hashCode()和equals(),所以记得重写这两个方法(一般使用Lombok的话问题不大)。
distinct()提供的去重功能比较简单,就是判断对象重复。如果希望实现更细粒度的去重,比如根据对象的某个属性去重,可以怎么做呢?可以参考:分享几种 Java8 中通过 Stream 对列表进行去重的方法
一般来说,学到已经覆盖实际开发90%的场景了,后面的可以不用学了。
高阶操作
两部分内容:
- 深化一下collect()方法,它还有很多其他玩法
- 介绍flatMap、reduce、匹配查找、peek、forEach等边角料
collect高阶操作
聚合
public class StreamTest {
private static List<Person> list;
static {
list = new ArrayList<>();
list.add(new Person("i", 18, "杭州", 999.9));
list.add(new Person("am", 19, "温州", 777.7));
list.add(new Person("iron", 21, "杭州", 888.8));
list.add(new Person("man", 17, "宁波", 888.8));
}
/**
* 演示用collect()方法实现聚合操作,对标max()、min()、count()
* @param args
*/
public static void main(String[] args) {
// 方式1:匿名对象
Optional<Person> max1 = list.stream().collect(Collectors.maxBy(new Comparator<Person>() {
@Override
public int compare(Person p1, Person p2) {
return p1.getAge() - p2.getAge();
}
}));
System.out.println(max1.orElse(null));
// 方式2:Lambda
Optional<Person> max2 = list.stream().collect(Collectors.maxBy((p1, p2) -> p1.getAge() - p2.getAge()));
System.out.println(max2.orElse(null));
// 方式3:方法引用
Optional<Person> max3 = list.stream().collect(Collectors.maxBy(Comparator.comparingInt(Person::getAge)));
System.out.println(max3.orElse(null));
// 方式4:IDEA建议直接使用 max(),不要用 collect(Collector)
Optional<Person> max4 = list.stream().max(Comparator.comparingInt(Person::getAge));
System.out.println(max4.orElse(null));
// 特别是方式3和方式4,可以看做collect()聚合和max()聚合的对比
// 剩下的minBy和counting
Optional<Person> min1 = list.stream().collect(Collectors.minBy(Comparator.comparingInt(Person::getAge)));
Optional<Person> min2 = list.stream().min(Comparator.comparingInt(Person::getAge));
Long count1 = list.stream().collect(Collectors.counting());
Long count2 = list.stream().count();
}
@Data
@AllArgsConstructor
@NoArgsConstructor
static class Person {
private String name;
private Integer age;
private String address;
private Double salary;
}
}
分组
public class StreamTest {
private static List<Person> list;
static {
list = new ArrayList<>();
list.add(new Person("i", 18, "杭州", 999.9));
list.add(new Person("am", 19, "温州", 777.7));
list.add(new Person("iron", 21, "杭州", 888.8));
list.add(new Person("man", 17, "宁波", 888.8));
}
/**
* 按字段分组
* 按条件分组
*
* @param args
*/
public static void main(String[] args) {
// GROUP BY address
Map<String, List<Person>> groupingByAddress = list.stream().collect(Collectors.groupingBy(Person::getAddress));
System.out.println(groupingByAddress);
// GROUP BY address, age
Map<String, Map<Integer, List<Person>>> doubleGroupingBy = list.stream()
.collect(Collectors.groupingBy(Person::getAddress, Collectors.groupingBy(Person::getAge)));
System.out.println(doubleGroupingBy);
// 简单来说,就是collect(groupingBy(xx)) 扩展为 collect(groupingBy(xx, groupingBy(yy))),嵌套分组
// 解决了按字段分组、按多个字段分组,我们再考虑一个问题:有时我们分组的条件不是某个字段,而是某个字段是否满足xx条件
// 比如 年龄大于等于18的是成年人,小于18的是未成年人
Map<Boolean, List<Person>> adultsAndTeenagers = list.stream().collect(Collectors.partitioningBy(person -> person.getAge() >= 18));
System.out.println(adultsAndTeenagers);
}
@Data
@AllArgsConstructor
@NoArgsConstructor
static class Person {
private String name;
private Integer age;
private String address;
private Double salary;
}
}
提一句,collect()方法是最为丰富的,可以搭配Collector玩出很多花样,特别是经过各种嵌套组合。本文末尾留了几道思考题,大家到时可以试着做做。
统计
public class StreamTest {
private static List<Person> list;
static {
list = new ArrayList<>();
list.add(new Person("i", 18, "杭州", 999.9));
list.add(new Person("am", 19, "温州", 777.7));
list.add(new Person("iron", 21, "杭州", 888.8));
list.add(new Person("man", 17, "宁波", 888.8));
}
/**
* 统计
* @param args
*/
public static void main(String[] args) {
// 平均年龄
Double averageAge = list.stream().collect(Collectors.averagingInt(Person::getAge));
System.out.println(averageAge);
// 平均薪资
Double averageSalary = list.stream().collect(Collectors.averagingDouble(Person::getSalary));
System.out.println(averageSalary);
// 其他的不演示了,大家自己看api提示。简而言之,就是返回某个字段在某个纬度的统计结果
// 有个更绝的,针对某项数据,一次性返回多个纬度的统计结果:总和、平均数、最大值、最小值、总数,但一般用的很少
IntSummaryStatistics allSummaryData = list.stream().collect(Collectors.summarizingInt(Person::getAge));
long sum = allSummaryData.getSum();
double average = allSummaryData.getAverage();
int max = allSummaryData.getMax();
int min = allSummaryData.getMin();
long count = allSummaryData.getCount();
}
@Data
@AllArgsConstructor
@NoArgsConstructor
static class Person {
private String name;
private Integer age;
private String address;
private Double salary;
}
}
flatMap
总的来说,就是flatMap就是把多个流合并成一个流:
public class StreamTest {
private static List<Person> list;
static {
list = new ArrayList<>();
list.add(new Person("i", 18, "杭州", 999.9, new ArrayList<>(Arrays.asList("成年人", "学生", "男性"))));
list.add(new Person("am", 19, "温州", 777.7, new ArrayList<>(Arrays.asList("成年人", "打工人", "宇宙最帅"))));
list.add(new Person("iron", 21, "杭州", 888.8, new ArrayList<>(Arrays.asList("喜欢打篮球", "学生"))));
list.add(new Person("man", 17, "宁波", 888.8, new ArrayList<>(Arrays.asList("未成年人", "家里有矿"))));
}
public static void main(String[] args) {
Set<String> allTags = list.stream().flatMap(person -> person.getTags().stream()).collect(Collectors.toSet());
System.out.println(allTags);
}
@Data
@AllArgsConstructor
@NoArgsConstructor
static class Person {
private String name;
private Integer age;
private String address;
private Double salary;
// 个人标签
private List<String> tags;
}
}
对于每个Person对象中的tags,如果没有flatMap,想要合并去重会比较麻烦。
对比map和flatMap:
flatMap传入的Function要求返回值是Stream<? extends R>而不是R,所以上面写的是:
Function返回的多个Stream<R>最终会被flatMap汇聚成同一个Stream<R>,而map的Function返回R,最终被收集成Stream<R>。
总之,当你遇到List中还有List,然后你又想把第二层的List都拎出来集中处理时,就可以考虑用flatMap(),先把层级打平,再统一处理。
forEach
这个,其实不算高阶操作,算了,就放这吧。简单来说就是遍历:
public class StreamTest {
private static List<Person> list;
static {
list = new ArrayList<>();
list.add(new Person("i", 18, "杭州", 999.9));
list.add(new Person("am", 19, "温州", 777.7));
list.add(new Person("iron", 21, "杭州", 888.8));
list.add(new Person("man", 17, "宁波", 888.8));
}
public static void main(String[] args) {
// 遍历操作,接收Consumer
list.stream().forEach(System.out::println);
// 简化,本质上不算同一个方法的简化
list.forEach(System.out::println);
}
@Data
@AllArgsConstructor
@NoArgsConstructor
static class Person {
private String name;
private Integer age;
private String address;
private Double salary;
}
}
peek()
它接受一个Consumer,一般有两种用法:
- 设置值
- 观察数据
设置值的用法:
public class StreamTest {
public static void main(String[] args) {
list.stream().peek(person -> person.setAge(18)).forEach(System.out::println);
}
}
也就是把所有人的年龄设置为18岁。
peek这个单词本身就带有“观察”的意思。
简单来说,就是查看数据,一般实际开发很少用,但可以用来观察数据的流转:
public class StreamTest {
public static void main(String[] args) {
Stream<Integer> stream = Stream.of(1, 2, 3);
stream.peek(v-> System.out.print(v+",")).map(value -> value + 100).peek(v-> System.out.print(v+",")).forEach(System.out::println);
}
}
结果
1,101,101
2,102,102
3,103,103
有没有感到好奇?
用图表示的话,就是这样:
通过peek,我们观察到每一个元素都是逐个通过Stream流的。
为了看得更清楚些,重新写个demo:
public static void main(String[] args) {
Stream.of(1, 2, 3, 4)
.peek(v -> System.out.print(v + ","))
.filter(v -> v >= 2)
.peek(v -> System.out.print(v + ","))
.filter(v -> v >= 3)
.forEach(System.out::println);
}
结果
1,2,2,3,3,3
4,4,4
这打印,怎么这么诡异?
其实不诡异,元素确实是逐个通过的:
一个元素如果中途被过滤,就不会继续往下,换下一个元素。最终串起来,就会打印:
1,2,2,3,3,3
4,4,4
大家思考一下,真正的Stream和我们之前山寨的Stream,遍历时有何不同?能画图解释一下吗?
元素一个个通过关卡
元素一起通过一个个关卡
匹配/查找
findFirst
public static void main(String[] args) {
Optional<Integer> first = Stream.of(1, 2, 3, 4)
.peek(v -> System.out.print(v + ","))
.findFirst();
}
结果:
1,
只要第一个,后续元素不会继续遍历。
findAny
public static void main(String[] args) {
Optional<Integer> any = Stream.of(1, 2, 3, 4)
.peek(v -> System.out.print(v + ","))
.findAny();
}
结果:
1,
findFirst和findAny几乎一样,但如果是并行流,结果可能不一致:
并行流颇有种“分而治之”的味道(底层forkjoin线程池),将流拆分并行处理,能较大限度利用计算资源,提高工作效率。但要注意,如果当前操作对顺序有要求,可能并不适合使用parallelStream。比如上图右边,使用并行流后返回的可能是4而不是1。
关于findFirst()和findAny()有没有人觉得这方法很傻逼?其实是我demo举的不对,通常来说应该是一堆filter操作任取一个等场景下使用。比如list.stream().filter(student->student.getAge()>18).findFirst(),即符合条件的任选一个。
allMatch
public static void main(String[] args) {
boolean b = Stream.of(1, 2, 3, 4)
.peek(v -> System.out.print(v + ","))
.allMatch(v -> v > 2);
}
结果
1,
由于是要allMatch,第一个就不符合,那么其他元素也就没必要测试了。这是一个短路操作。
就好比:
if(0>1 && 2>1){
// 2>1 不会被执行,因为0>1不成立,所以2>1被短路了
}
noneMatch
public static void main(String[] args) {
boolean b = Stream.of(1, 2, 3, 4)
.peek(v -> System.out.print(v + ","))
.noneMatch(v -> v >= 2);
}
结果
1,2,
和allMatch一样,它期望的是没有一个满足,而2>=2已经false,后面元素即使都大于2也不影响最终结果:noneMatch=false,所以也是个短路操作。
anyMatch
略
reduce
略,实际开发没用过,大家可以在熟悉上面的用法后再去了解,否则可能比较乱。
Stream API的效率问题
public class Test {
public static void main(String[] args) {
// 1. 简单数据类型:整数
testSimpleType();
// 2. 复杂数据类型:对象
// testObjectType();
}
private static void testSimpleType() {
Random random = new Random();
List<Integer> integerList = new ArrayList<>();
for (int i = 0; i < 10000000; i++) {
integerList.add(random.nextInt(Integer.MAX_VALUE));
}
// 1) stream
testStream(integerList);
// 2) parallelStream
testParallelStream(integerList);
// 3) 普通for
testForLoop(integerList);
// 4) 增强型for
testStrongForLoop(integerList);
// 5) 迭代器
testIterator(integerList);
}
private static void testObjectType() {
Random random = new Random();
List<Product> productList = new ArrayList<>();
for (int i = 0; i < 10000000; i++) {
productList.add(new Product("pro" + i, i, random.nextInt(Integer.MAX_VALUE)));
}
// 1) stream
testProductStream(productList);
// 2) parallelStream
testProductParallelStream(productList);
// 3) 普通for
testProductForLoop(productList);
// 4) 增强型for
testProductStrongForLoop(productList);
// 5) 迭代器
testProductIterator(productList);
}
// -------- 测试简单类型 --------
public static void testStream(List<Integer> list) {
long start = System.currentTimeMillis();
Optional<Integer> optional = list.stream().max(Integer::compare);
System.out.println("result=" + optional.orElse(0));
long end = System.currentTimeMillis();
System.out.println("testStream耗时:" + (end - start) + "ms");
}
public static void testParallelStream(List<Integer> list) {
long start = System.currentTimeMillis();
Optional<Integer> optional = list.parallelStream().max(Integer::compare);
System.out.println("result=" + optional.orElse(0));
long end = System.currentTimeMillis();
System.out.println("testParallelStream耗时:" + (end - start) + "ms");
}
public static void testForLoop(List<Integer> list) {
long start = System.currentTimeMillis();
int max = Integer.MIN_VALUE;
for (int i = 0; i < list.size(); i++) {
int current = list.get(i);
if (current > max) {
max = current;
}
}
System.out.println("result=" + max);
long end = System.currentTimeMillis();
System.out.println("testForLoop耗时:" + (end - start) + "ms");
}
public static void testStrongForLoop(List<Integer> list) {
long start = System.currentTimeMillis();
int max = Integer.MIN_VALUE;
for (Integer integer : list) {
if (integer > max) {
max = integer;
}
}
System.out.println("result=" + max);
long end = System.currentTimeMillis();
System.out.println("testStrongForLoop耗时:" + (end - start) + "ms");
}
public static void testIterator(List<Integer> list) {
long start = System.currentTimeMillis();
Iterator<Integer> it = list.iterator();
int max = it.next();
while (it.hasNext()) {
int current = it.next();
if (current > max) {
max = current;
}
}
System.out.println("result=" + max);
long end = System.currentTimeMillis();
System.out.println("testIterator耗时:" + (end - start) + "ms");
}
// -------- 测试对象类型 --------
public static void testProductStream(List<Product> list) {
long start = System.currentTimeMillis();
Optional<Product> optional = list.stream().max((p1, p2) -> p1.hot - p2.hot);
System.out.println(optional.orElseThrow(() -> new RuntimeException("对象不存在")));
long end = System.currentTimeMillis();
System.out.println("testProductStream耗时:" + (end - start) + "ms");
}
public static void testProductParallelStream(List<Product> list) {
long start = System.currentTimeMillis();
Optional<Product> optional = list.parallelStream().max((p1, p2) -> p1.hot - p2.hot);
System.out.println(optional.orElseThrow(() -> new RuntimeException("对象不存在")));
long end = System.currentTimeMillis();
System.out.println("testProductParallelStream耗时:" + (end - start) + "ms");
}
public static void testProductForLoop(List<Product> list) {
long start = System.currentTimeMillis();
Product maxHot = list.get(0);
for (int i = 0; i < list.size(); i++) {
Product current = list.get(i);
if (current.hot > maxHot.hot) {
maxHot = current;
}
}
System.out.println(maxHot);
long end = System.currentTimeMillis();
System.out.println("testProductForLoop耗时:" + (end - start) + "ms");
}
public static void testProductStrongForLoop(List<Product> list) {
long start = System.currentTimeMillis();
Product maxHot = list.get(0);
for (Product product : list) {
if (product.hot > maxHot.hot) {
maxHot = product;
}
}
System.out.println(maxHot);
long end = System.currentTimeMillis();
System.out.println("testProductStrongForLoop耗时:" + (end - start) + "ms");
}
public static void testProductIterator(List<Product> list) {
long start = System.currentTimeMillis();
Iterator<Product> it = list.iterator();
Product maxHot = it.next();
while (it.hasNext()) {
Product current = it.next();
if (current.hot > maxHot.hot) {
maxHot = current;
}
}
System.out.println(maxHot);
long end = System.currentTimeMillis();
System.out.println("testProductIterator耗时:" + (end - start) + "ms");
}
}
@Data
@AllArgsConstructor
class Product {
// 名称
String name;
// 库存
Integer stock;
// 热度
Integer hot;
}
大家把测试案例拷贝到本地test包下运行看看,相信心中有判断。虽然上面的案例中没有测试list.stream.filter().map.dictinc()等连环操作,但结果应该差不多。
但我想说的是,绝大部分时候代码的可读性应该优先于性能,况且Stream API性能并不差。和传统代码相比,Stream API让程序员专注于实现的步骤而不是细节,大大提高了代码的可读性。
Integer minPrice = itemSkuPriceTOS.stream()
.sorted(Comparator.comparingLong(ItemSkuPriceTO::getPrice)) // 先正序排列
.findFirst() // 找到第一个,也就是价格最低的item
.map(ItemSkuPriceTO::getPrice) // 得到item的价格
.orElse(0); // 兜底处理
如果你习惯了Stream API,上面的代码在可读性上会比用for循环实现好得多,所以能用Stream API的尽量用Stream API吧。毕竟上面测试用了1000w数据差距也就几十毫秒,更别说实际项目中可能就几十条了。
最后再来看思维导图复习一下吧(有些使用频率很低,本文就略过了):
作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO
进群,大家一起学习,一起进步,一起对抗互联网寒冬