java8用流收集数据

收集器简介 Collector

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

收集器用作高级规约

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

List<Transaction> transactions = transactionStream.collect(Collectors.toList());
image

规约和汇总

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

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

还可以这样写更为直接:

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

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

找出菜单中热量最高的菜。你可以使用两个收集器,Collectors.maxBy()和Collectors.minBy()

你可以创建一个Comparator来根据所含热量对菜肴进行比较,并把Collectors.maxBy:

Optional<Dish> mostCalorieDish = menu.stream().collect(maxBy((a1,a2)—>a1.getCalories - a2.getCalories));

汇总 summingInt summingDouble

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

int totalCalories = menu.stream().collect(summingInt((v)-> v.getCalories()));
image

汇总不仅仅求和;还有Collectors.averageingInt,连同对应的averagingInt等计算数值的平均数。

int averageCalories = menu.stream().collect(averagintInt((a) -> a.getCalories()));

到目前为止,已经看到了如何使用手机器来给流中的元素计数,找到这些元素数值属性的最大值和最小值,以及计算其总和和平均值。不过很多时候,你可能想要得到两个或更多这样的结果,而且你希望只需一次操作就可以完成。这种情况下,你可以就输出菜单中的元素个数,并得到菜肴热量的总和,平均值,最大值和最小值。

IntSummaryStatastics menuStatistics = menu.stream().collect(summarizingInt(Dish::getCalories));

这个收集器会把所有这些信息收集到一个叫做IntSummaryStatistics的类里面,他提供了方便的取值(getter)方法来访问结果。打印menuStatisticobject会得到下面的结果:

IntSummaryStatistics{count=9,sum=4300,min=120,average=47.7778,max=800}

连接成字符串

joining工厂方法返回的收集器会把对流中每一个对象应用toString方法得到的所有字符串连接成一个字符串。这意味着你把菜单中所有的菜肴的名称连接起来。

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

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

String shortMenu = menu.stream().collect(jioning());

但该字符串的可读性并不好。幸好,joining的工厂方法有一个重载版本可以接受元素之间的分解符,这样你就可以得到一个逗号分隔符的菜肴名称列表:

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

到目前为止,我们已经探讨了各种将流归约到一个值的收集器。在下一节中,我们会展示为什么所有这种形式的归约过程,其实都是Collectors.reducing工厂方法提供的更广义归约收集器的特殊的情况。

广义的归约汇总

事实上,我们已经讨论的所有的收集器,都是一个可以用reducing的工厂方法定义的归约过程的特殊情况二期。Collectors.reducing工厂方法是所有这些特殊情况的一般化。列如,可以用reducing方法创建的收集器来计算你菜单的总热量,如下:

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

同样,你可以使用下面这样单参数形式的reducing来找到热量最高的菜,如下所示:

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

分组

一个常见的数据库操作是根据一个或多个属性对集合中的项目进行分组。就像按货币对交易进行分组,如果用指令式风格来实现的话,这个操作可能会很麻烦,啰嗦,容易出错。但是用java8的话很容易看懂。举一个列子:假设要把菜单中的菜按照类型进行分类,有肉的放一组,有鱼的放一组,其他的放一组。用Collectors.groupingBy工厂方法返回的收集器就可以轻松的完成这项任务。

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

这里,你给groupingBy方法传递了一个Function(以方法引用的形式),它提取了流中每一道Dish的Dish.Type。我们把这个Function叫做分类函数,因为它用来把流中的元素分成不同的组。分组结果时一个Map,把分组函数返回的值座位映射的建,把流中所有具有这个分类值的项目的列表座位对应的映射值。在菜单分裂的例子中,键就是菜的类型,值就是包含所有对应类型的菜肴列表。

image

多级分组

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

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

分区

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

Map<Boolean,List<Dish>> partitionedMenu = menu.stream().collect(partitioningBy(Dish::isVegetarian));

那么通过Map中键位true的值,就可以找出所有的素食菜肴了:

List<Dish> vegetarianDishes = partitionedMenu.get(true);

收集器接口 Collector

Collector接口包含了一系列方法,为实现具体的归约操作提供了范本。我们已经看过了Collector接口中实现的许多收集器,列如toList或者groupingBy.这也意味着,你可以为Collector接口提供自己的实现,从而自由地创建自定义归约操作。

Collector接口定义

public interface Collector<T,A,R>{
    Suppier<A> supplier();
    BiConsumer<A,T> accumulator();
    Function<A,R> finisher();
    BinaryOperator<A> combiner();
    Set<Charactoristics> characteristics();
}

T :是流要收集的项目的泛型。

A :是累加器的类型,累加器是在收集过程中用于累积部分结果的对象。

R :是手机操作得到的对象(通常但并不一定是集合)的类型。

例如,你可以实现一个ToListCollector<T> 类,将Stream<T>中的所有元素收集到一个List<T>里,它的签名如下:

public class ToListCollector<T> implements Collector<T,List<T>,List<T>>

理解Collector接口声明的方法

现在我们可以一个一个来分析Collector接口声明的五个方法了。通过分析,你会注意到,前四个方法都会返回一个会被collect方法调用的函数,而第五个方法characteristics则提供了一系列特征,也就是一个提示列表,告诉collect方法在执行归约操作的时候可以应用那些优化(比如并行化)

建立新的结果容器:supplier方法

supplier方法必须返回一个结果为空Suppier,也就是一个无参数函数,在调用时它会创建一个空的累加器实例,供数据收集过程使用。很明显,对于将累加器本身作为结果返回的收集器,比如我们的ToListCollector,在对空流执行操作的时候,这个空的累加器也代表了收集过程的结果。在我们的ToListCollector中,supplier返回一个空的List

public Supplier<List<T>> supplier(){
    return ()->new ArrayList<T>();
}

请注意你也可以值传递一个构造函数引用:

public Supplier<List<T>> supplier(){
    return ArrayList::new;
}

将元素添加到结果容器:accumulator方法

accumulator方法会返回执行归约操作的函数。当遍历到流中的第n个元素是,这个函数执行时会有两个参数:保存归约结果的累加器(已经收集了流中的前n-1个项目),还有第n个元素本身。该函数将返回void,因为累加器是原位更新,即函数的执行改变了它的内部状态以体现遍历的元素效果。对于ToListCollector,这个函数仅仅会把当前项目添加至已经遍历过的项目的列表:

public BiConsumer<List<T>,T> accumulator(){
    return (list,item) -> list.add(item);
}

也可以使用方法引用,这会更简洁:

public BiConsumer<List<T>,T> accumulator(){
    return List::add;
}

对结果容器应用最终转化:finisher方法

在遍历完流后,finisher方法必须返回在累积过程的最后要调用一个函数,以便将累加器对象转换为整个集合操作的最终效果。通常,就像ToListCollector的情况一些样,累加器对象恰好复合预期的最终效果,因此无需转换。所以finisher方法只需返回identity函数:

public Funciton<List<T>,List<T>> finisher(){
    reutrn Function.identity();
}
image

合并两个结果容器:combiner方法

四个方法中的最后一个——combiner方法会返回一个供归约操作使用的函数,它定义了对流的各个子部分进行并行处理时,各个子部分归约所得的累加器要如何合并。对于toList而言,这个方法的实现非常简单,只要把从流的第二部分收集到的项目列表加到遍历第一部分时得到的列表后面就行了:

public BinaryOperator<List<T>> combiner(){
    return (list1,list2)->{
        list1.addAll(list2);
        return list1;
    }
}
image
  • 原始流会以递归方式拆分为子流,直到定义流是否需要进一步拆分的一个条件为非(如果分布式工作单位太小,并行计算往往比顺序计算要慢,而且要是生成的并行任务比处理器内核书多的话就毫无意义了)
  • 现在,所有的子流都可以并行处理,即对每个子流应用上图的顺序归约算法
  • 最后,使用收集器combiner方法返回函数,将所有的部分结果两两合并。这时会把原始流每次拆分时得到的子流对应的结果和并起来。

characteristics方法

最后一个方法——characteriestics会返回一个不可表你的Characteristics集合,它定义了收集器的行为——尤其是关于可以并行归约,以及可以使用那些优化的提示。Characteristics是一个包含三个项目的枚举。

  • UNORDERED——归约结果不受流中项目的遍历和累积顺序的影响。
  • CONCURRENT——accumulator函数可以从多个线程同时调用,且该收集器可以并行归约流。如果收集器没有表为UNORDERED,那它尽在用于无需数据源时才可以并行归约。
  • IDENTITY_FINISH——这表明完成器方法返回的函数是一个恒等函数,可以跳过。这种情况下,累加器对象将会直接用作归约过程的最终结果。这也意味着,将累加器A不加检查的转化为结果R是安全的。

ToListCollector是IDENTITY_FINISH的,因为用来累积流中元素的List已经是我们要的最终结果,用不着进一步转换了,但他并不是UNORDERED,因为用在有序留上的时候,我们还是希望顺序能够保留在得到的List中。最后,他是CONCURRENT的,但我们刚才说过了,仅仅在背后的数据源无序时才会进行并行处理

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容

  • Java8 in action 没有共享的可变数据,将方法和函数即代码传递给其他方法的能力就是我们平常所说的函数式...
    铁牛很铁阅读 1,208评论 1 2
  • Int Double Long 设置特定的stream类型, 提高性能,增加特定的函数 无存储。stream不是一...
    patrick002阅读 1,267评论 0 0
  • Java 8 数据流教程 原文:Java 8 Stream Tutorial 译者:飞龙 协议:CC BY-NC-...
    布客飞龙阅读 934评论 1 46
  • 收集器简介 汇总 并行流 欢迎访问本人博客查看原文:http://wangnan.tech 收集器简介 对流调用c...
    GhostStories阅读 1,435评论 1 7
  • 收集器可以简洁而灵活地定义collect用来生成结果集合的标准。更具体地说,对流调用 collect 方法将对流中...
    刘涤生阅读 1,688评论 0 1