Java 8 Stream 流如何使用2-用流收集数据

本文深入探讨了Java 8 Stream API中的收集器,包括预定义收集器的使用,如查找最大值、最小值、汇总和连接字符串。重点介绍了归约、分组和分区操作,以及如何自定义收集器以提升性能。通过实例展示了如何使用Collectors类创建和使用收集器,以及开发自己的收集器来优化处理流程。
摘要由CSDN通过智能技术生成

上一篇讲了流可以用类似于数据库的操作帮助你处理集合。在本篇中,你会发现collect是一个归约操作,就像reduce一样可以接受各种做法作为参数,将流中的元素累积成一个汇总结果。具体的做法是通过定义新的Collector接口来定义的,因此区分Collection、Collector和collect是很重要的。

本篇内容:

  • 用Collectors类创建和使用收集器
  • 将数据流归约为一个值
  • 汇总:归约的特殊情况
  • 数据分组和分区
  • 开发自己的自定义收集器

1. 收集器简介

前一个例子清楚地展示了函数式编程相对于指令式编程的一个主要优势:你只需指出希望的结果——“做什么”,而不用操心执行的步骤——“如何做”。
要是做多级分组,指令式和函数式之间的区别就会更加明显:由于需要好多层嵌套循环和条件,指令式代码很快就变得更难阅读、更难维护、更难修改。相比之下,函数式版本只要再加上一个收集器就可以轻松地增强功能了。

1.1 预定义收集器

    在本节主要探讨预定义收集器的功能,也就是那些可以从Collectors类提供的工厂方法(例如groupingBy)创建的收集器。它们主要提供了三大功能:

  • 将流元素归约和汇总为一个值
  • 元素分组
  • 元素分区

2. 归约和汇总

在需要将流项目重组成集合时,一般会使用收集器(Stream方法collect的参数)。再宽泛一点来说,但凡要把流中所有的项目合并成一个结果时就可以用。这个结果可以是任何类型,可以复杂如代表一棵树的多级映射,或是简单如一个整数。
也许代表了菜单的热量总和:

        //我们先来举一个简单的例子,利用counting工厂方法返回的收集器,数一数菜单里有多少种菜
        Long howManyDishes = menu.stream().collect(Collectors.counting());
        //这还可以写得更为直接:
        long howManyDishes = menu.stream().count();
        counting收集器在和其他收集器联合使用的时候特别有用,后面会谈到这一点。
2.1 查找流中的最大值和最小值

你可以使用两个收集器,Collectors.maxBy和Collectors.minBy,来计算流中的最大或最小值。这两个收集器接收一个Comparator参数来比较流中的元素。

        /**
         * 你可以使用两个收集器,Collectors.maxBy和
         * Collectors.minBy,来计算流中的最大或最小值。这两个收集器接收一个Comparator参数来
         * 比较流中的元素。
         */
        List<Dish> menu1 = Arrays.asList(new Dish("苹果", false, 20, Dish.Type.MEAT),
                new Dish("梨子", false, 30, Dish.Type.MEAT),
                new Dish("黄瓜", true, 60, Dish.Type.MEAT),
                new Dish("白菜", true, 20, Dish.Type.FISH),
                new Dish("水稻", false, 40, Dish.Type.OTHER)
        );

        Comparator<Dish> dishComparator = Comparator.comparingInt(Dish::getCalories);
        Optional<Dish> mostCalorieDish = menu1.stream()
                .collect(maxBy(dishComparator));
2.2 汇总

Collectors类专门为汇总提供了一个工厂方法:Collectors.summingInt。它可接受一个把对象映射为求和所需int的函数,并返回一个收集器;该收集器在传递给普通的collect方法后即执行我们需要的汇总操作。

你可以这样求出菜单列表的总热量:

int totalCalories = menu1.stream().collect(summingInt(Dish::getCalories));

Collectors.summingLong和Collectors.summingDouble方法的作用完全一样,可以用于求和字段为long或double的情况。
但汇总不仅仅是求和;还有Collectors.averagingInt,连同对应的averagingLong和averagingDouble可以计算数值的平均数。

Double avgCalories = menu1.stream().collect(averagingInt(Dish::getCalories));

通过一次summarizing操作你可以就数出菜单中元素的个数,并得到菜肴热量总和、平均值、最大值和最小值:

 //同样,相应的summarizingLong和summarizingDouble工厂方法有相关的LongSummaryStatistics和DoubleSummaryStatistics类型,适用于收集的属性是原始类型long或double的情况。
  IntSummaryStatistics summaryStatistics = menu1.stream().collect(summarizingInt(Dish::getCalories));
        System.out.println("summaryStatistics =>"+summaryStatistics);
2.3 连接字符串

joining工厂方法返回的收集器会把对流中每一个对象应用toString方法得到的所有字符串连接成一个字符串。

 String nameStr = menu1.stream().map(Dish::getName).collect(joining());

请注意,joining在内部使用了StringBuilder来把生成的字符串逐个追加起来。此外还要注意,如果Dish类有一个toString方法来返回菜肴的名称,那你无需用提取每一道菜名称的函数来对原流做映射就能够得到相同的结果:

String nameMenu = menu1.stream().collect(joining());
//有分隔符或者有前后缀
String nameMenu = menu1.stream().map(Dish::getName).collect(joining(", "));

//示例: 1,2,3,4,5  拼接结果('1','2','3','4','5')  ('苹果','梨子','黄瓜','白菜','水稻')
String nameMenu1 = menu1.stream().map(Dish::getName).collect(joining("','","('","')"));
System.out.println("nameMenu1=>"+nameMenu1);
2.4 广义的归约汇总

事实上,我们已经讨论的所有收集器,都是一个可以用reducing工厂方法定义的归约过程的特殊情况而已。Collectors.reducing工厂方法是所有这些特殊情况的一般化。可以说,先前讨论的案例仅仅是为了方便程序员而已。

用reducing方法创建的收集器来计算你菜单的总热量
Integer totalCalories = menu1.stream().collect(reducing(0, Dish::getCalories, (i, j) -> i + j));

如上代码,它需要三个参数。

  • 第一个参数是归约操作的起始值,也是流中没有元素时的返回值,所以很显然对于数值和而言0是一个合适的值。
  • 第二个参数就是将菜肴转换成一个表示其所含热量的int。
  • 第三个参数是一个BinaryOperator,将两个项目累积成一个同类型的值。这里它就是对两个int求和。
    同样,你可以使用下面这样单参数形式的reducing来找到热量最高的菜,如下所示:
        Optional<Dish> maxDish = menu1.stream().collect(reducing(
                (a, b) -> a.getCalories() > b.getCalories() ? a : b));


int totalCalories = menu.stream().mapToInt(Dish::getCalories).sum();
2.4.1 收集框架的灵活性:以不同的方法执行同样的操作
//三种方法
int totalCalories = menu.stream().collect(reducing(0, 
 Dish::getCalories,Integer::sum));
 
int totalCalories = 
 menu.stream().map(Dish::getCalories).reduce(Integer::sum).get();
 
 int totalCalories = menu.stream().mapToInt(Dish::getCalories).sum();
2.4.2 根据情况选择最佳解决方案

我们的建议是,尽可能为手头的问题探索不同的解决方案,但在通用的方案里面,始终选择最专门化的一个。无论是从可读性还是性能上看,这一般都是最好的决定。

3. 分组

一个常见的数据库操作是根据一个或多个属性对集合中的项目进行分组。就像前面讲到按货币对交易进行分组的例子一样,如果用指令式风格来实现的话,这个操作可能会很麻烦、啰嗦而且容易出错。但是,如果用Java 8所推崇的函数式风格来重写的话,就很容易转化为一个非常容易看懂的语句。

我们来看看这个功能的第二个例子:假设你要把菜单中的菜按照类型进行分类,
有肉的放一组,有鱼的放一组,其他的都放另一组。用Collectors
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值