Java8 Stream API

作者简介:大家好,我是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@NoArgsConstructorstatic 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@NoArgsConstructorstatic 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>() {@Overridepublic int compare(Person p1, Person p2) {return p1.getName().length()-p2.getName().length();}});// JDK8之前:List本身也实现了sort()list.sort(new Comparator<Person>() {@Overridepublic 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@NoArgsConstructorstatic 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@NoArgsConstructorstatic 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@NoArgsConstructorstatic 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个方法// 把结果收集为ListList<String> toList = list.stream().map(Person::getAddress).collect(Collectors.toList());System.out.println(toList);// 把结果收集为SetSet<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@NoArgsConstructorstatic 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@AllArgsConstructorstatic class Person {private String name;private Integer age;private String address;private Double salary;@Overridepublic 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>() {@Overridepublic 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@NoArgsConstructorstatic 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@NoArgsConstructorstatic 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>() {@Overridepublic int compare(Person p1, Person p2) {return p1.getAge() - p2.getAge();}}));System.out.println(max1.orElse(null));// 方式2:LambdaOptional<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和countingOptional<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@NoArgsConstructorstatic 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 addressMap<String, List<Person>> groupingByAddress = list.stream().collect(Collectors.groupingBy(Person::getAddress));System.out.println(groupingByAddress);// GROUP BY address, ageMap<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@NoArgsConstructorstatic 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@NoArgsConstructorstatic 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@NoArgsConstructorstatic 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) {// 遍历操作,接收Consumerlist.stream().forEach(System.out::println);// 简化,本质上不算同一个方法的简化list.forEach(System.out::println);}@Data@AllArgsConstructor@NoArgsConstructorstatic 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) streamtestStream(integerList);// 2) parallelStreamtestParallelStream(integerList);// 3) 普通fortestForLoop(integerList);// 4) 增强型fortestStrongForLoop(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) streamtestProductStream(productList);// 2) parallelStreamtestProductParallelStream(productList);// 3) 普通fortestProductForLoop(productList);// 4) 增强型fortestProductStrongForLoop(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

进群,大家一起学习,一起进步,一起对抗互联网寒冬

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/232202.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

私家车位共享APP-计算机毕业设计源码24384

目 录 摘要 1 绪论 1.1 课题的研究背景 1.2研究内容与研究目标 1.3ssm框架 1.4论文结构与章节安排 2 2 私家车位共享APP系统分析 2.1 可行性分析 2.2 系统流程分析 2.2.1 数据增加流程 2.2.2 数据修改流程 2.2.3数据删除流程 2.3 系统功能分析 2.3.1功能性分析 2…

【SpringCloud原理】OpenFeign原来是这么基于Ribbon来实现负载均衡的

大家好&#xff0c;本文我将继续来剖析SpringCloud中负载均衡组件Ribbon的源码。本来我是打算接着OpenFeign动态代理生成文章直接讲Feign是如何整合Ribbon的&#xff0c;但是文章写了一半发现&#xff0c;如果不把Ribbon好好讲清楚&#xff0c;那么有些Ribbon的细节理解起来就很…

mac安装homebrew/brew遇到443

文章目录 问题描述解决方法方法一方法二 参考文献 问题描述 brew 全称Homebrew 是Mac OSX上的软件包管理工具 想在mac终端安装&#xff0c;运行网上提供的指令 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)&quo…

EMA训练微调

就是取前几个epoch的weight的平均值&#xff0c;可以缓解微调时的灾难性遗忘&#xff08;因为新数据引导&#xff0c;模型权重逐渐&#xff0c;偏离训练时学到的数据分布&#xff0c;忘记之前学好的先验知识&#xff09; class EMA():def __init__(self, model, decay):self.…

TikTok区块链实践:数字社交媒体的去中心化未来

随着区块链技术的日渐成熟&#xff0c;数字社交媒体行业也在探索如何整合区块链&#xff0c;以推动去中心化发展。在这一潮流中&#xff0c;TikTok作为全球领先的短视频平台&#xff0c;积极实践区块链技术&#xff0c;探索数字社交媒体的未来。本文将深入探讨TikTok的区块链实…

功能全面又强大的同步备份软件,你找到了吗?

随着企业规模的不断扩大&#xff0c;许多企业都会拥有自己的数据中心。因此每日员工都需要在服务器与服务中心之间调取文件&#xff0c;同时还需要对每日新增的业务数据进行实时同步。如果量比较小&#xff0c;一般问题不大&#xff1b;一旦数据比较大&#xff0c;量也比较大&a…

箭头函数与普通函数:谁更胜一筹?

&#x1f90d; 前端开发工程师&#xff08;主业&#xff09;、技术博主&#xff08;副业&#xff09;、已过CET6 &#x1f368; 阿珊和她的猫_CSDN个人主页 &#x1f560; 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 &#x1f35a; 蓝桥云课签约作者、已在蓝桥云…

酷开科技:让体育迷的热情释放,让运动精神传递

在繁忙的生活节奏中&#xff0c;我们总是被各种琐事所困扰&#xff0c;很难抽出时间去享受运动带来的快乐&#xff0c;甚至很少有时间去观看一场体育赛事。而一场好的体育赛事带给体育爱好者的快乐往往来自于两方面&#xff0c;一是线下参与&#xff0c;感受现场带来的震撼&…

docker搭建node环境开发服务器

docker搭建node环境开发服务器 本文章是我自己搭建node环境开发服务器的过程记录&#xff0c;不一定完全适用所有人。根据个人情况&#xff0c;按需取用。 命名项目路径 为了方便cd到项目路径&#xff0c;将项目路径重命名&#xff0c;方便输入。 vim /etc/profile # 修改p…

R语言gWQS包在加权分位数和回归模型的应用

在流行病学研究中&#xff0c;相较于单一因素的暴露&#xff0c;多因素同时暴露的情况更为常见。传统模型在评价多因素联合暴露时存在数据维度高、多重共线性等问题. WQS 回归模型的基本原理是通过分位数间距及加权的方法&#xff0c;将多种研究因素的效应综合成为一个指数&…

7-22 龟兔赛跑

import java.util.Scanner; class Main {public static void main(String[] args) {Scanner scnew Scanner(System.in);int timesc.nextInt();sc.close();int wugui 0;//乌龟里程int tuzi 0;//兔子里程int tuzi_run0;int tuzi_rest0;int is_rest0;//是否需要休息&#xff1a;…

visual studio 2022 更改字体和大小

工具--->选项 文本编辑器 输出窗口