Java 8-Stream API-用流收集数据

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/zsx157326/article/details/80903821

用指令使风格对交易按照年份分组

    @Test
    public void test9() {

        //建立根据年份分组的Map
        Map<Integer,List<Transaction>> transactionByCurrencies=new HashMap<>();

        //遍历Transaction的List
        for (Transaction transaction : transactions) {
            //提取Transaction的年份
            Integer year = transaction.getYear();

            List<Transaction> transactionsForCurrency= transactionByCurrencies.get(year);

            //如果分组Map中没有这个年份,就创建一个
            if(transactionsForCurrency==null){
                transactionsForCurrency=new ArrayList<>();
                transactionByCurrencies.put(year,transactionsForCurrency);
            }
            //将当前遍历的Transaction加入同一年份的Transaction的List
            transactionsForCurrency.add(transaction);

        }

        System.out.println(transactionByCurrencies);
    }

用流收集数据

Map<Integer,List<Transaction>> transactionByCurrencies=
    transactions.stream().collect(groupingBy(Transaction::getCurrency));

收集器简介

函数式编程相对于指令式编程的一个主要优势:你只需指出希望的结果—做什么,而不用操心执行的步骤—如何做。

groupingBy说的是“生成一个Map,它的键是(货币)桶”,值则是桶中那些元素的列表。

优秀的函数式API设计的另一个好处:更易复合和重用。收集器非常有用,因为用它可以简洁而灵活地定义collect用来生成结果集合的标准。更具体地说,对流调用collect方法将对流中的元素触发一个归约操作(由Collector来参数化)

一般来说,Collector会对元素应用一个转换函数(很多时候是不体现任何效果的恒等转换,例如toList),并将结果积累在一个数据结构中,从而产生这一过程的最终输出。例如,在前面所示的交易分组的例子中,转换函数提取了每笔交易的时间,随后使用货币作为键,将交易本身累积在生成的Map中。

Collector接口中方法的实现决定了如何对流执行归约操作。我们可以创建自定义收集器。但Collectors实用类提供了很多静态工厂方法,可以方便地创建常见收集器的实例,只要拿来用就可以了。最直接和最常用的收集器是toList静态方法,它会把流中所有的元素收集到一个List中。

预定义的收集器

从Collectors类提供的工厂方法(例如groupingBy)创建的收集器。它们主要提供了三大功能:

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

归约和汇总

利用counting工厂方法返回的收集器,数一数菜单里有多少种菜

long howManyDishes=menu.stream().collect(Collectors.counting());

这还可以写得更为直接:

long howManyDishes=menu.stream().count();

counting收集器在和其他收集器联合使用的时候特别有用。

在后面的部分,我们假定你已导入了Collectors类的所有静态工厂方法:

import static java.util.stream.Collectors.*;

这样你就可以写counting()而用不着写Collectors.counting()之类的了。

查找流中的最大值和最小值

找出菜单中热量最高的菜。Collectors.maxBy和Collectors.minBy。这两个收集器接收一个Comparator参数来比较流中的元素。

Comparator<Dish> dishColoriesComparator=
    Comparator.comparing(Dish::getCalories);
Optional<Dish> mostCalorieDish=
    menu.stream()
        .collect(maxBy(dishCaloriesComparator));

汇总

Collectors.summingInt。它可接受一个把对象映射为求和所需int的函数,并返回一个收集器;该收集器在传递给普通的collect方法后即执行我们需要的汇总操作。可以这样求出菜单列表的总热量:

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

在遍历流时,会把每一道菜都映射为其热量,然后把这个数字累加到一个累加器(这里的初始值为0)

Collectors.summingLong和Collectors.summingDouble方法的作用完全一样,可以用于求和字段为long或double的情况。

但汇总不仅仅是求和;还有Collectors.averagingInt,连同对应的averagingLong和averagingDouble可以计算数值的平均数

double avgCalories=
    menu.stream().collect(averagingInt(Dish::getCalories));

有时候,你可能想要得到两个或更多这样的结果,而且你希望只需一次操作就可以完成。在这种情况下,你可以使用summarizingInt工厂方法返回的收集器。例如,通过一次summarizing操作你可以就数出菜单中元素的个数,并得到菜肴热量总和、平均值、最大值和最小值。

IntSummaryStatistics menuStatistics=menu.stream().collect(summarizingInt(Dish::getColories));
System.out.println(menuStatistics.getCount());

同样,相应的summarizingLong和summarizingDouble工厂方法有相关的LongSummaryStatistics和DoubleSummaryStatistics类型,适用于收集的属性是原始类型long或double的情况。

连接字符串

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

String shortMenu=menu.stream().map(Dish::getName).collect(joining());

joining在内部使用了StringBuilder来把生成的字符串逐个追加起来。

joining工厂方法有一个重载版本可以接受元素之间的分界符。

String shortMenu=menu.stream().map(Dish::getName).collect(joining(","));

广义的归约汇总

我们已经讨论的所有收集器,都是一个可以用reducing工厂方法定义的归约过程的特殊情况而已。Collectors.reducing工厂方法是所有这些特殊情况的一般化。

可以用reducing方法创建的收集器来计算你菜单的总热量

int totalCalories=menu.stream().collect(reducing(0,Dish::getCalories,(i,j)->i+j));

它需要三个参数:

  • 第一个参数是归约操作的起始值,也是流中没有元素时的返回值
  • 第二个参数是
  • 第三个参数是一个BinaryOperator,

可以使用单参数形式的reducing来找到热量最高的菜

Optional<Dish> mostCalorieDish=
    menu.stream().collect(reducing(
        (d1,d2)->d1.getCalories()>d2.getCalories()?d1:d2));

可以把单参数reducing工厂方法创建的收集器看作三参数方法的特殊情况,它把流中的第一个项目作为起点,把恒等函数(即第一个函数仅仅是返回其输入参数)作为一个转换函数。这也意味着,要是把单参数reducing收集器传递给空流的collect方法,收集器就没有起点。

reducing方法有三个重载函数

收集与归约

Stream接口的collect和reduce方法有何不同,因为两种方法通常会获得相同的结果。

可以像下面这样使用reduce方法来实现toListCollector所作的工作:

Stream<Integer> stream=Arrays.asList(1,2,3,4,5,6).stream();
List<Integer> numbers=stream.reduce(
    new ArrayList<>(), 
    (List<Integer> l, Integer e)->{
        l.add(e);
        return l;
    },
    (List<Integer> l1,List<Integer> l2)->{
        l1.addAll(l2);
        return l1;
    });

这个解决方案有两个问题:一个语义问题和一个实际问题。语义问题在于,reduce方法旨在把两个值结合起来生成一个新值,它是一个不可变的归约。与此相反,collect方法的设计就是要改变容器,从而积累要输出的结果。这意味着,上面的代码片段是在滥用reduce方法,因为它在原地改变了作为累加器的List。

以错误的语义使用reduce方法还会造成一个实际问题:这个归约过程不能并行工作,因为由多个线程并发修改同一个数据结构可能会破坏List本身。在这种情况下,如果你想要线程安全,就需要每次分配一个新的List,而对象分配又会影响性能。这就是collect方法特别适合表达可变容器上的归约的原因,更关键的是它适合并行操作。

函数式编程提供了多种方法来执行同一个操作。收集器在某种程度上比Stream接口上直接提供的方法用起来更复杂,但好处在于它们能提供更高水平的抽象和概括,可更容易重用和自定义。

建议,尽可能为手头的问题探索不同的解决方案,但在通用的方案里面,始终选择最专门化的一个。

分组

用Collectors.groupingBy工厂方法返回的收集器就可以轻松地完成这项任务。

Map<Dish.Type,List<Dish>> dishesByType=menu.stream().collect(groupingBy(Dish::getType));

其结果是下面的Map

{OTHER=[french fries, rice, season fruit, pizza], FISH=[prawns, salmon], MEAT=[pork, beef, chicken]}

给groupingBy方法传递了一个Function(以方法引用的形式),它提取了流中每一道Dish的Dish.Type。把这个Function叫作分组函数,因为它用来把流中的元素分成不同的组。

分组操作的结果是一个Map,把分组函数返回的值作为映射的键,把流中所有具有这个分类值的项目的列表作为对应的映射值。在菜单分类的例子中,键就是菜的类型,值就是包含所有对应类型的菜肴的列表。

但是,分类函数不一定像方法引用那样可用,因为你想用以分类的条件可能比简单的属性访问器要复杂。例如,你可能想把热量不到400卡路里的菜划分为“低热量”(diet),热量400到700卡路里的菜划分为“普通”(normal),高于700卡路里的划为“高热量”(fat)。

由于Dish类的作者没有把这个操作写成一个方法,你无法使用方法引用,但你可以把这个逻辑写成Lambda表达式:

public enum CaloricLevel{DIET,NORMAL,FAT}
Map<CaloricLevel,List<Dish>> dishesByCaloricLevel=menu.stream().collect(
    groupingBy(dish->{
        if (dish.getColories()<=400) return CaloricLevel.DIET;
        else if(dish.getColories()<=700) return CaloricLevel.NORMAL;
        else return CaloricLevel.FAT;
    }));

多级分组

要实现多级分组,我们可以使用一个由双参数版本的Collectors.groupingBy工厂方法创建的收集器,它除了普通的分类函数之外,还可以接受collector类型的第二个参数。那么要进行二级分组的话,我们可以把一个内层groupingBy传递给外层groupingBy,并定义一个为流中项目分类的二级标准。

Map<Dish.Type,Map<CaloricLevel,List<Dish>>> dishesByTypeCaloricLevel=menu.stream().collect(
                groupingBy(Dish::getType,
                        groupingBy(dish->{
                            if (dish.getColories()<=400) return CaloricLevel.DIET;
                            else if(dish.getColories()<=700) return CaloricLevel.NORMAL;
                            else return CaloricLevel.FAT;
                        }))
        );

    }

二级分组的结果就是像下面这样的两极Map:

{OTHER={DIET=[rice], FAT=[season fruit], NORMAL=[french fries, pizza]}, FISH={DIET=[prawns], NORMAL=[salmon]}, MEAT={DIET=[chicken], FAT=[pork], NORMAL=[beef]}}

这里的外层Map的键就是第一级分类函数生成的值“fish,meat,other”,而这个Map的值又是一个Map,键是二级分类函数生成的值“normal,diet,fat”。

这种多级分组操作可以扩展至任意层级,n级分组就会得到一个代表n级树形结果的n级Map

把groupingBy看作“桶”比较容易明白。第一个groupingBy给每个键建立了一个桶。然后再利用下游的收集器去收集每个桶中的元素,以此得到n级分组。

按子组收集数据

可以把第二个groupingBy收集器传递给外层收集器来实现多级分组。但进一步说,传递给第一个groupingBy的第二个收集器可以是任何类型,而不一定是另一个groupingBy。例如,要数一数菜单中每类菜有多少个,可以传递counting收集器作为groupingBy收集器的第二个参数。

Map<Dish.Type,Long> typesCount=menu.stream().collect(
    groupingBy(Dish::getType,counting()));

普通的单参数groupingBy(f)实际上是groupingBy(f,toList())的简便写法

查找每类菜中热量最高的菜肴

 Map<Dish.Type,Optional<Dish>> mostCaloricByType=
                menu.stream()
                        .collect(groupingBy(Dish::getType,maxBy(comparingInt(Dish::getColories))));

这个Map中的值是Optional,因为这是maxBy工厂方法生成的收集器的类型,但实际上,如果菜单中没有某一类型的Dish,这个类型就不会对应一个Optional.empty()值,而且根本不会出现在Map的键中。groupingBy收集器只有在应用分组条件后,第一次在流中找到某个键对应的元素时才会把键加入分组Map中。这意味Optional包装器在这里不是很有用,因为它不会仅仅因为它是归约收集器的返回类型而表达一个最终可能不存在却意外存在的值。

1.把收集器的结果转换为另一种类型

因为分组操作的Map结果中每个值上包装的Optional没什么用,所以你可能想要把它们去掉。把收集器返回的结果转换为另一种类型,你可以使用Collectors.collectingAndThen工厂方法返回的收集器。

Map<Dish.Type,Dish> mostCaloricByType=menu.stream()
    .collect(groupingBy(Dish::getType,
                        collectingAndThen(
                            maxBy(comparingInt(Dish::getColories)),Optional::get)));

这个工厂方法接受两个参数—要转换的收集器以及转换函数,并返回另一个收集器。

2.与groupingBy联合使用的其他收集器的例子

求每种类型菜肴热量的总和

Map<Dish.Type,Integer> totalCaloriesByType=
        menu.stream().collect(groupingBy(Dish::getType,
                           summingInt(Dish::getCalories)));

然而常常和groupingBy联合使用的另一个收集器是mapping方法生成的。这个方法接受两个参数:一个函数对流中的元素做变换,另一个则将变换的结果对象收集起来。其目的是在累加之前对每个输入元素应用一个映射函数,这样就可以让接受特定类型元素的收集器适应不同类型的对象。

需求:对于每种类型的Dish,菜单中都有哪些CaloricLevel。

Map<Dish.Type,Set<CaloricLevel>> caloricLevelsByType= menu.stream().collect(
    groupingBy(Dish::getType,mapping(
        dish->{
            if (dish.getColories()<=400) return CaloricLevel.DIET;
            else if(dish.getColories()<=700) return CaloricLevel.NORMAL;
            else return CaloricLevel.FAT;
        },toSet()))
);

上面对于返回的Set是什么类型并没有任何保证。但通过使用toCollection,你就可以有更多的控制。例如,你可以给它传递一个构造函数引用来要求HashSet:

Map<Dish.Type,Set<CaloricLevel>> caloricLevelsByType= menu.stream().collect(
    groupingBy(Dish::getType,mapping(
        dish->{
            if (dish.getColories()<=400) return CaloricLevel.DIET;
            else if(dish.getColories()<=700) return CaloricLevel.NORMAL;
            else return CaloricLevel.FAT;
        },toCollection(HashSet::new)))
);

分区

分区是分组的特殊情况:由一个谓词(返回一个布尔值的函数)作为分类函数,它称为分区函数。

分区函数返回一个布尔值,这意味着得到的分组Map的键类型是Boolean,于是它最多可以分为两组—true是一组,false是一组。例如,如果你是素食者或是请了一位素食的朋友来共进晚餐,可能会想要把菜单按照素食和非素食分开。

Map<Boolean,List<Dish>> partitionedMenu=menu.stream().collect(
    partitioningBy(Dish::isVegetarian)
);
{false=[pork, beef, chicken, season fruit, prawns, salmon], true=[french fries, rice, pizza]}

用同样的分区谓词,对菜单List创建的流作筛选,然后把结果收集到另外一个List中也可以获得相同的结果:

List<Dish> vegetarianDishes=menu.stream().filter(Dish::isVegetarian).collect(toList());

分区的优势

分区的好处在于保留了分区函数返回true或false的两套元素列表。

partitioningBy工厂方法有一个重载版本,可以像下面这样传递第二个收集器:

 Map<Boolean,Map<Dish.Type,List<Dish>>> vegetarianDishesByType=menu.stream().collect(
               partitioningBy(Dish::isVegetarian,groupingBy(Dish::getType)));
{false={MEAT=[pork, beef, chicken], FISH=[prawns, salmon], OTHER=[season fruit]}, true={OTHER=[french fries, rice, pizza]}}
工厂方法 返回类型 用于
toList List<T> 把流中所有项目收集到一个List
toSet Set<T> 把流中所有项目收集的一个Set,删除重复项
toCollection Collectiion<T> 把流中所有项目收集到给定的供应源创建的集合
counting Long 计算流中元素的个数
summingInt Integer 对流中项目的一个整数属性求和
averagingInt Double 计算流中项目Integer属性的平均值
summarizingInt IntSummaryStatistics 收集关于流中项目Integer属性的统计值,例如最大、最小、总和与平均值
joining String 连接对流中每个项目调用toString方法所生成的字符串
maxBy Optional<T> 一个包裹了流中按照给定比较器选出的最大元素的Optional,或如果流为空则为Optional.empty()
minBy Optional<T> 一个包裹了流中按照给定比较器选出的最小元素的Optional,或如果流为空则为Optional.empty()
reducing 归约操作产生的类型 从一个作为累加器的初始值开始,利用BinaryOperator与流中元素逐个结合,从而将流归约为单个值
collectingAndThen 转换函数返回的类型 包裹另一个收集器,对其结果应用转换函数
groupingBy Map<K,List<T>> 根据项目的一个属性的值对流中的项目作为组,并将属性值作为结果Map的键
partitioningBy Map<Boolean,List<T>> 根据对流中每个项目应用谓词的结果来对项目进行分区

小结

  • collect是一个终端操作,它接受的参数是将流中元素累积到汇总结果的各种方式(称为收集器)
  • 预定义收集器包括将流元素归约和汇总到一个值,例如计算最小值、最大值或平均值
  • 预定义收集器可以用groupingBy对流中元素进行分组,或用partitioningBy进行分区
  • 收集器可以高效地复合起来,进行多级分组、分区和归约
  • 可以实现Collector接口中定义的方法来开发你自己的收集器
展开阅读全文

没有更多推荐了,返回首页