Java8 Stream API使用
博客专区 > 青离 的博客 > 博客详情
Java8 Stream API使用
青离 发表于1周前
Java8 Stream API使用
  • 发表于 1周前
  • 阅读 341
  • 收藏 52
  • 点赞 0
  • 评论 1

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/

标签: Stream Java
共有 人打赏支持
粉丝 198
博文 30
码字总数 77942
评论 (1)
wonderWang
不错,总结的挺全面的
×
青离
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
* 金额(元)
¥1 ¥5 ¥10 ¥20 其他金额
打赏人
留言
* 支付类型
微信扫码支付
打赏金额:
已支付成功
打赏金额: