Java8 Stream API使用


声明:本文转载自https://my.oschina.net/u/2377110/blog/1573456,转载目的在于传递更多信息,仅供学习交流之用。如有侵权行为,请联系我,我会及时删除。

Java8面世后,目前工作中使用最多的特性就当是Stream API。Stream API结合lamada表达式带来的是全新的编程体验,以往一些繁琐的数据处理,如今不仅条理清晰,而且代码量至少减少了一半。Stream API使用一段时间后,我就不满足于零碎的一些常用组合方式,希望对Stream有一个轮廓性的认识。因而在参考了官方文档和众多网上资料后,自己对Stream API做了一些小总结,以期达到梳理和记录之用。

1.认识Stream

Java8对Stream的定义是这样的

A sequence of elements supporting sequential and parallel aggregate operations.

简单的翻译就是支持顺序和并行的汇聚操作的一组元素

然而这个解释看上去没有什么用,更通俗的说法是Stream是一个高级的Iterator,相对于原始的Iterator显示地遍历元素并执行操作,Stream在其内部隐式地进行数据转换。

另外Stream也可以理解成一个管道,它并非数据结构,不保存数据,input的数据从管道一头进入,管道中完成定义好的处理方式,从另一头输出你要的结果。因而Stream是单向的,不可往复的,想要多次使用只有创建新的Stream。

Stream的处理过程可分为三个部分,我们举个例子,对1,2,3三个数的平方求和。

List<Integer> numbers = Arrays.asList(1, 2, 3); numbers.stream().mapToInt(n -> n * n).sum(); 

以上的操作包含了Stream的三部分,分别是创建Stream转换Stream聚合Stream

下面就依次分析下这三个部分

2.创建Stream

常用的创建Stream的方式有三种:

  1. 通过集合类的相关方法
  2. 通过Stream接口的静态工厂方法
  3. 其他能产生流的类的方法

2.1 集合类的相关方法

Collection接口中定义了一个stream方法

default Stream<E> stream() {     return StreamSupport.stream(spliterator(), false); } 

因而所有Collection的子类都能通过此方法开启Stream,常见的就是从数据库查询出一个List,然后使用Stream进行处理。

List<User> userList = // 数据库查询 long teenagerNum = userList.stream().filter(u -> u.getAge() < 20).count(); 

从数据库查询出一组User,获得出年龄小于20岁的用户数。

还有一种情况是,参数是一个数组,可以使用Arrays工具类来处理

int[] nums = {1, 2, 3, 2}; Arrays.stream(nums).distinct().forEach(System.out::println); 

distinct方法用来去重,即将一组整数去重后打印出来

2.2 Stream静态工厂方法

Stream接口中也提供多种静态方法来辅助我们构造Stream

1.of方法,非常好用的方法,接受可变参数

Stream<String> stringStream = Stream.of("1", "2", "3"); 

或者直接传入数组

String[] strs = {"1", "2", "3"}; Stream<String> stringStream = Stream.of(strs); 

2.generator方法,生成一个无限长度的Stream,需要一个自定义的Supplier类,比如创建一个随机数的Stream

Stream.generate(new Supplier<Long>() { 
    @Override     public Long get() {         return Math.random();     } }); 

可以使用lamada表达式及Method Reference简化

Stream.generate(() -> Math.random()); Stream.generate(Math::random); 

一般无限长度的Stream都会配合Stream的limit方法来使用

3.iterate方法,同样生成无限长度的Stream,但它是对给定的种子(seed)反复调用用户指定函数来生成,其生成的元素可认为是:seed,f(seed),f(f(seed))无限循环

Stream.iterate(1, i -> i + 1).limit(10).forEach(System.out::println); 

4.concat方法,将两个Stream组合成一个Stream

Stream<String> stream1 = Stream.of("1", "2", "3"); Stream<String> stream2 = Stream.of("4", "5"); Stream.concat(stream1, stream2); 

5.builder方法,使用追加的方式建立Stream然后消费

Stream.builder()     .add("1")     .add("2")     .build(); 

2.3 其他方式

Java也在其他可能有Stream操作的类中增加了便捷的方法

// 文件读取 java.io.BufferedReader.lines() // 文件遍历 java.nio.file.Files.walk() // 随机数 java.util.Random.ints() // 正则匹配 Pattern.splitAsStream(java.lang.CharSequence) 

3.转换Stream

转换Stream就是把一个Stream通过某些行为转换成新的Stream。Stream接口中定义了常用的几个转换方法,但在实际使用中确实大大的方便。

3.1 distinct

对于Stream中包含的元素进行去重(依赖元素的equals方法)

之前对一个List进行去重,都是借助HashSet来做

List list2 = new ArrayList(new HashSet(list)); 

现在也可以使用distinct来做

List list2 = list.stream().distinct().collect(Collectors.toList()); 

3.2 filter

对Stream中包含的元素使用给定的过滤函数进行过滤,新生成的Stream只包含符合条件的元素

List<Integer> numbers = Arrays.asList(-1, 1, 0); numbers.stream().filter(n -> n > 0).collect(Collectors.counting()); 

3.3 map

对Stream中包含的元素使用给定的转换函数进行转换,新生成的Stream只包含转换生成的元素

Stream.of(1, 2, 3).map(n -> n + 1).collect(Collectors.toList()); 

这个方法由三个对于原始数据类型的变种方法,分别是mapToIntmapToLong, mapToDouble, 主要是减少自动装箱和拆箱的性能消耗。

3.4 flatMap

和map相似,但一般用来将多层级扁平化,举个例子大家就清楚了。

有个两层结构的数据结构

[ [1, 2, 3], [4, 5], [6] ] 

把它全部摊平

[1, 2, 3, 4, 5, 6] 

就可以使用flatMap

Stream<List<Integer>> intStream = Stream.of(Arrays.asList(1, 2, 3),Arrays.asList(4,5),Arrays.asList(6));         intStream.flatMap(childList -> childList.stream()).forEach(System.out::println); 

3.5 peek

生成一个包含原Stream所有元素的新Stream,同时提供一个消费函数(Consume),当Stream中每个元素被消费时都会执行给定的消费函数。

举个例子来说,对1到9求和,求和前打印所有求和元素。因为Stream是单向的,做了求和就不能再打印了,怎么办呢?用peek方法。

int sum = IntStream.range(1, 10).peek(System.out::println).sum(); System.out.println("sum:" + sum); 

3.6 limit

对Stream进行截断操作,获取其前N个元素,如果原Stream中包含元素个数小于N,就获取其所有元素。

IntStream.range(1, 10).limit(5).forEach(System.out::println); 

3.7 skip

丢弃Stream前N个元素,返回剩下元素组成的新Stream,如果原Stream中包含的元素个数小于N,返回空Stream。

比如打印一个数组的第10到20个元素

IntStream.range(1, 100).skip(10).limit(10).forEach(System.out::println); 

3.8 sorted

对Stream中的元素按默认方式排序或者指定比较器排序

Stream.of(1, 3, 4 ,2).sorted().forEach(System.out::println); 

4.聚合Stream

聚合操作接受一个元素序列作为输入,反复使用某个合并操作,把序列中的元素合并成一个汇总的结果。比如求元素的总和或最大值,或者把元素累积成一个List对象。Stream接口有一些通用的汇聚操作,比如reduce和collect;也有特定用途的汇聚操作,比如sum,max,count。

4.1 collect

将Stream中的元素收集到一个结果容器中。java8还为collect方法提供了工具类Collectors,可以方便地生成List,Set,Map等集合。

// 获取userId的List List<Long> userIdList = users.stream().map(u -> u.getId()).collect(Collectors.toList()); // 获取userId的Set Set<Long> userIdSet = users.stream().map(u -> u.getId()).collect(Collectors.toSet()); // 获取userId的Map Map<Long, User> userIdMap = users.stream().collect(Collectors.toMap(u -> u.getId(), u -> u)); 

另外Collectors还提供了两种非常常用的聚合方式:分组和分片,分别对应groupingBy和partitioningBy两个方法。怎么用呢,我们来看例子。

我想将用户按所在地址分组,北京的放在一起,上海的放在一起,可以用groupingBy。

Map<String, List<User>> userAddressMap = users.stream().collect(Collectors.groupingBy(User::getAddress)); 

或者我想将用户按年龄区分,20岁以上为一个分片,20岁以下为一个分片,可以用partitioningBy。

Map<Boolean, List<User>> userAgeMap = users.stream().collect(Collectors.partitioningBy(u -> u.getAge() > 20)); 

这两个的区别也就显而易见了,groupingBy是按照某个内部字段进行分组,而partitioningBy是按照某个条件将元素分成是和非两个分片。

Collectors中还有其他很便捷的方法,有兴趣的可以研究下。

4.2 reduce

上面说的collect可以看成对Stream元素进行不同方式的聚集,而reduce则是对Stream元素进行指定方式的聚合。

比如我想求用户的最大年龄,可以使用reduce来操作。

Optional optional = users.stream().map(User::getAge).reduce((a1, a2) -> a1 > a2 ? a1 : a2); 

这个方法返回值是Optional,它是java8防止出现空指针的一种方式。要获得最大的年龄,调用Optional的get方法即可。

Integer maxAge = users.stream().map(User::getAge).reduce((a1, a2) -> a1 > a2 ? a1 : a2).get(); 

但是这种方式还是可能会出现空指针,我们可以在reduce时设置一个默认值。

Integer maxAge = users.stream().map(User::getAge).reduce(0 ,(a1, a2) -> a1 > a2 ? a1 : a2); 

这样就不需要Optional做一次转换了。

像最大,最小,计数这些是常见的计算方式,在Stream中也都提供了直接的方法供我们使用。

  • max(Comparator comp)
  • min(Comparator comp)
  • count()

对于max,min方法需要提供Compartor比较器。如果是在诸如mapToInt,mapToLong,mapToDouble之类的基础数据操作后,则不用提供比较器,另外还支持求和(sum)和平均(average)方法。

users.stream().mapToInt(User::getAge).sum(); users.stream().mapToInt(User::getAge).average(); 

4.3 搜索相关

Stream API还提供了几种快捷的搜索方法,支持在一组元素中的常用搜索。

  • allMatch:所有元素都满足匹配条件
  • anyMatch:任一元素满足匹配条件
  • findFirst:返回Stream中的第一个元素
  • findAny:返回Stream中的任意个元素
  • noneMatch:所有元素都不满足匹配条件

4.4 遍历

将Stream中的元素逐个按照指定方式进行消费。

打印所有用户

users.stream().forEach(System.out::println); 

5.Stream API的优点

  1. Stream转换操作是惰性化的(lazy),即多次转换操作在聚合操作时只需一次循环就能完成,并不会因为多次转换造成额外的循环开销。
  2. 因为1的原因,Stream的数据源可以是无限的,它并不会像普通iterator一样需要把所有数据都加载到内存中。比如Stream的generate方法和limit方法相结合,可以从无限的数据源中操作自己想要的数据。
  3. Stream可以并行化操作,它依赖于java7中引入的Fork/Join框架来拆分任务和加速处理过程。对于Collection子类,直接使用parallelStream方法即可开启并行化操作。不过并行化也是会有额外的开销的,因此要适当地使用。

6.Stream API用例

以库存实体Stock为例

public class Stock {      // 主键id     Long id;     // 商品id     Long skuId;     // 供应商id     int supplierId;     // 状态(0:不可用,1:可用,2:任务中)     int status;     // 库存量     BigDecimal amount; } 

一般都是根据条件从数据库查询出一个Stock的List,变量名为stockList,从这个List出发,介绍一些常用的Stream的用例。

6.1 从大集合中获取小集合

// 获取id的集合 List<Long> idList = stockList.stream().map(Stock::getId).collect(Collectors.toList()); // 获取skuid集合并去重 List<Long> skuIdList = stockList.stream().map(Stock::getSkuId).distinct().collect(Collectors.toList()); // 获取supplierId集合(supplierId的类型为int,返回List<Integer>,使用boxed方法装箱) Set<Integer> supplierIdSet = stockList.stream().mapToInt(Stock::getSupplierId).boxed().collect(Collectors.toSet()); 

6.2 分组与分片

// 按skuid分组 Map<Long, List<Stock>> skuIdStockMap = stockList.stream().collect(Collectors.groupingBy(Stock::getSkuId)); // 过滤supplierId=1然后按skuId分组 Map<Long, List<Stock>> filterSkuIdStockMap = stockList.stream().filter(s -> s.getSupplierId() == 1).collect(Collectors.groupingBy(Stock::getSkuId)); // 按状态分为不可用和其他两个分片 Map<Boolean, List<Stock>> partitionStockMap = stockList.stream().collect(Collectors.partitioningBy(s -> s.getStatus() == 0)); 

6.3 计数与求和

// 统计skuId=1的记录数 long skuIdRecordNum = stockList.stream().filter(s -> s.getSkuId() == 1).count(); // 统计skuId=1的总库存量 BigDecimal skuIdAmountSum = stockList.stream().filter(s -> s.getSkuId() == 1).map(Stock::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add); 

6.4 特定用法

// 多重分组并排序,先按supplierId分组,再按skuId分组,排序规则,先supplierId后skuId Map<Integer, Map<Long, List<Stock>>> supplierSkuStockMap = stockList.stream().collect(Collectors.groupingBy(Stock::getSupplierId, TreeMap::new,                 Collectors.groupingBy(Stock::getSkuId, TreeMap::new, Collectors.toList())));  // 多条件排序,先按supplierId正序排,再按skuId倒序排 // (非stream方法,而是集合的sort方法,直接改变原集合元素,使用Function参数) stockList.sort(Comparator.comparing(Stock::getSupplierId)                 .thenComparing(Stock::getSkuId, Comparator.reverseOrder())); 

参考文档

http://ifeve.com/stream/ (强烈推荐,非常清晰的介绍)

http://www.infoq.com/cn/articles/java8-new-features-new-stream-api

https://www.ibm.com/developerworks/cn/java/j-lo-java8streamapi/

本文发表于2017年11月14日 18:33
(c)注:本文转载自https://my.oschina.net/u/2377110/blog/1573456,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责。如有侵权行为,请联系我们,我们会及时删除.

阅读 1975 讨论 0 喜欢 0

抢先体验

扫码体验
趣味小程序
文字表情生成器

闪念胶囊

你要过得好哇,这样我才能恨你啊,你要是过得不好,我都不知道该恨你还是拥抱你啊。

直抵黄龙府,与诸君痛饮尔。

那时陪伴我的人啊,你们如今在何方。

不出意外的话,我们再也不会见了,祝你前程似锦。

这世界真好,吃野东西也要留出这条命来看看

快捷链接
网站地图
提交友链
Copyright © 2016 - 2021 Cion.
All Rights Reserved.
京ICP备2021004668号-1