JAVA8 stream中三个参数的reduce方法对List进行分组统计操作

背景

平时在编写前端代码时,习惯使用lodash来编写‘野生'的JavaScript;

lodash提供来一套完整的API对JS对象(Array,Object,Collection等)进行操作,这其中就包括_.groupBy 和 _.reduce,即分组和'聚合'(reduce不知道该怎么翻译合适)。

使用这些‘野生'的API能够极大的提高我本人编写JS代码的效率。而JAVA8开始支持stream和lambda表达式,这些和lodash的API有很多类似的功能。因此我在熟悉lodash的前提下尝试使用JAVA8的新特性减少冗余代码的编写。

需求

在开发后端某功能接口的过程中,需要对一个从数据库中取出的数据List<T>进行按照ID进行聚合统计

JAVA8 reduce API

API个人理解

<U> U reduce(U u,BiFunction<U,? super T,U> accumulator,BinaryOperator<U> combiner) #第一个参数返回实例u,传递你要返回的U类型对象的初始化实例u #第二个参数累加器accumulator,可以使用二元ℷ表达式(即二元lambda表达式),声明你在u上累加你的数据来源t的逻辑 #例如(u,t)->u.sum(t),此时lambda表达式的行参列表是返回实例u和遍历的集合元素t,函数体是在u上累加t #第三个参数组合器combiner,同样是二元ℷ表达式,(u,t)->u #lambda表达式行参列表同样是(u,t),函数体返回的类型则要和第一个参数的类型保持一致

伪代码

 #1.声明一个返回结果U
 #2.对List<T>进行遍历,在U和每个T实例上应用一个累加器进行累加操作
 #3.返回最终结果U
 U result = identity;
 for (T element : this stream)
   result = accumulator.apply(result, element)
 return result;

数据准备

var source =
[
  {"name": "A","type": "san","typeValue": 1.0,"count": 2},
  {"name": "A","type": "nas","typeValue": 13.0,"count": 1},
  {"name": "B","type": "san","typeValue": 112.0,"count": 3},
  {"name": "C","type": "san","typeValue": 43.0,"count": 5},
  {"name": "B","type": "nas","typeValue": 77.0,"count": 7}
];
var target =
[
  {
    "name": "A",
    "count": 3,
    "totalTypeValue": 14.0,
    "bazList": [
      {
        "type": "san",
        "typeValue": 1.0
      },
      {
        "type": "nas"
        "typeValue": 13.0
      }
    ]
  },
  {
    "name": "B",
    "count": 10,
    "totalTypeValue": 189.0,
    "bazList": [
      {
        "type": "san",
        "typeValue": 112.0
      }, {
        "type": "nas"
        "typeValue": 77.0
      }
    ]
  },
  {
    "name": "C",
    "count": 5,
    "totalTypeValue": 43.0,
    "bazList": [
      {
        "type": "san",
        "typeValue": 43.0
      }
    ]
  }
];

Code

讲了那么多废话,这个才是最直接的

代码执行大意

对 List<Foo> 按照name分组统计得到 List<Bar>

ReduceTest.java

import com.google.common.collect.Lists;
import Bar;
import Foo;
import java.util.List;
import java.util.stream.Collectors; 

public class ReduceTest {

  public static void main(String[] args) throws Exception{
    List<Foo> fooList = Lists.newArrayList(
      new Foo("A","san",1.0,2),
      new Foo("A","nas",13.0,1),
      new Foo("B","san",112.0,3),
      new Foo("C","san",43.0,5),
      new Foo("B","nas",77.0,7)
    );
    List<Bar> barList = Lists.newArrayList();
    fooList
      .stream()
      .collect(Collectors.groupingBy(Foo::getName,Collectors.toList()))
      .forEach((name,fooListByName)->{
        Bar bar = new Bar();
        bar = fooListByName
            .stream()
            .reduce(bar,(u,t)->u.sum(t),(u,t)->u);
        System.out.println(bar.toString());
        barList.add(bar);
      });
  }
  /*
  输出结果
  name:A
  count:3
  totalTypeValue:14.0
  bazList:
    type:san
    typeValue:1.0
    type:nas
    typeValue:13.0

  name:B
  count:10
  totalTypeValue:189.0
  bazList:
    type:san
    typeValue:112.0
    type:nas
    typeValue:77.0

  name:C
  count:5
  totalTypeValue:43.0
  bazList:
    type:san
    typeValue:43.0
  */
}

Foo.java

public class Foo{
  private String name;
  private String type;
  private Double typeValue;
  private Integer count;

  public Foo(String name, String type, Double typeValue, Integer count) {
    this.name = name;
    this.type = type;
    this.typeValue = typeValue;
    this.count = count;
  }

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public String getType() {
    return type;
  }

  public void setType(String type) {
    this.type = type;
  }

  public Double getTypeValue() {
    return typeValue;
  }

  public void setTypeValue(Double typeValue) {
    this.typeValue = typeValue;
  }

  public Integer getCount() {
    return count;
  }

  public void setCount(Integer count) {
    this.count = count;
  }
}

Bar.java

import com.google.common.collect.Lists;
import java.util.List;
public class Bar{
  private String name;
  private Integer count;
  private Double totalTypeValue;
  private List<Baz> bazList;

  public Bar() {
    this.name = null;
    this.count = 0;
    this.totalTypeValue = 0.0;
    this.bazList = Lists.newArrayList();
  }

  public Bar sum(Foo foo){
    if(name == null){
      this.name = foo.getName();
    }
    this.count += foo.getCount();
    this.totalTypeValue += foo.getTypeValue();
    this.bazList.add(new Baz(foo.getType(),foo.getTypeValue()));
    return this;
  }

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public Integer getCount() {
    return count;
  }

  public void setCount(Integer count) {
    this.count = count;
  }

  @Override
  public String toString() {
    StringBuilder sb = new StringBuilder();
    sb.append("name:").append(this.name).append(System.lineSeparator());
    sb.append("count:").append(this.count).append(System.lineSeparator());
    sb.append("totalTypeValue:").append(this.totalTypeValue).append(System.lineSeparator());
    sb.append("bazList:").append(System.lineSeparator());
    this.bazList.forEach(baz->{
      sb.append("\t").append("type:").append(baz.getType()).append(System.lineSeparator());
      sb.append("\t").append("typeValue:").append(baz.getTypeValue()).append(System.lineSeparator());
    });
    return sb.toString();
  }
}

Baz.java

public class Baz{
  private String type;
  private Double typeValue;

  public Baz(String type, Double typeValue) {
    this.type = type;
    this.typeValue = typeValue;
  }

  public String getType() {
    return type;
  }

  public void setType(String type) {
    this.type = type;
  }

  public Double getTypeValue() {
    return typeValue;
  }

  public void setTypeValue(Double typeValue) {
    this.typeValue = typeValue;
  }
}

PS

等下次有空补上不使用stream().reduce 实现同样操作的比较繁琐的代码,啦啦啦啦啦~~~

补充知识:Java8collect、reduce方法聚合操作详解

Stream的基本概念

Stream和集合的区别:

1.Stream不会自己存储元素。元素储存在底层集合或者根据需要产生。

2.Stream操作符不会改变源对象。相反,它会返回一个持有结果的新的Stream。

3.Stream操作可能是延迟执行的,这意味着它们会等到需要结果的时候才执行。

Stream操作的基本过程,可以归结为3个部分:

创建一个Stream。

在一个或者多个操作中,将指定的Stream转换为另一个Stream的中间操作。

通过终止(terminal)方法来产生一个结果。该操作会强制它之前的延时操作立即执行,这之后该Stream就不能再被使用了。

中间操作都是filter()、distinct()、sorted()、map()、flatMap()等,其一般是对数据集的整理(过滤、排序、匹配、抽取等)。

终止方法往往是完成对数据集中数据的处理,如forEach(),还有allMatch()、anyMatch()、findAny()、 findFirst(),数值计算类的方法有sum、max、min、average等等。终止方法也可以是对集合的处理,如reduce()、 collect()等等。

reduce()方法的处理方式一般是每次都产生新的数据集,而collect()方法是在原数据集的基础上进行更新,过程中不产生新的数据集。

List nums = Arrays.asList(1, 3, null, 8, 7, 8, 13, 10);

nums.stream().filter(num -> num != null).distinct().forEach(System.out::println);

上面代码实现为过滤null值并去重,遍历结果,实现简洁明了。使用传统方法就相对繁琐的多。另外其中 forEach即为终止操作方法,如果无该方法上面代码就没有任何操作。

filter、map、forEach、findAny等方法的使用都比较简单,这里省略。

下面介绍强大的聚合操作,其主要分为两种:

可变聚合:把输入的元素们累积到一个可变的容器中,比如Collection或者StringBuilder;

其他聚合:除去可变聚合,剩下的,一般都不是通过反复修改某个可变对象,而是通过把前一次的汇聚结果当成下一次的入参,反复如此。比如reduce,count,allMatch;

聚合操作reduce

Stream.reduce,返回单个的结果值,并且reduce操作每处理一个元素总是创建一个新值。常用的方法有average, sum, min, max, count,使用reduce方法都可实现。

这里主要介绍reduce方法:

T reduce(T identity, BinaryOperator accumulator)

identity:它允许用户提供一个循环计算的初始值。accumulator:计算的累加器,其方法签名为apply(T t,U u),在该reduce方法中第一个参数t为上次函数计算的返回值,第二个参数u为Stream中的元素,这个函数把这两个值计算apply,得到的和会被赋值给下次执行这个方法的第一个参数。有点绕看代码:

int value = Stream.of(1, 2, 3, 4).reduce(100, (sum, item) -> sum + item);
Assert.assertSame(value, 110);
/* 或者使用方法引用 */
value = Stream.of(1, 2, 3, 4).reduce(100, Integer::sum);

这个例子中100即为计算初始值,每次相加计算值都会传递到下一次计算的第一个参数。

reduce还有其它两个重载方法:

Optional reduce(BinaryOperatoraccumulator):与上面定义基本一样,无计算初始值,所以他返回的是一个Optional。

U reduce(U identity, BiFunction accumulator, BinaryOperator combiner):与前面两个参数的reduce方法几乎一致,你只要注意到BinaryOperator其实实现了BiFunction和BinaryOperator两个接口。

收集结果collect

当你处理完流时,通常只是想查看一下结果,而不是将他们聚合为一个值。先看collect的基础方法,它接受三个参数:

R collect(Supplier supplier, BiConsumer accumulator, BiConsumer combiner)

supplier:一个能创造目标类型实例的方法。accumulator:一个将当元素添加到目标中的方法。combiner:一个将中间状态的多个结果整合到一起的方法(并发的时候会用到)。接着看代码:

Stream stream = Stream.of(1, 2, 3, 4).filter(p -> p > 2);

List result = stream.collect(() -> new ArrayList<>(), (list, item) -> list.add(item), (one, two) -> one.addAll(two));
/* 或者使用方法引用 */
result = stream.collect(ArrayList::new, List::add, List::addAll);

这个例子即为过滤大于2的元素,将剩余结果收集到一个新的list中。

第一个方法生成一个新的ArrayList;

第二个方法中第一个参数是前面生成的ArrayList对象,第二个参数是stream中包含的元素,方法体就是把stream中的元素加入ArrayList对象中。第二个方法被反复调用直到原stream的元素被消费完毕;

第三个方法也是接受两个参数,这两个都是ArrayList类型的,方法体就是把第二个ArrayList全部加入到第一个中;

代码有点繁琐,或者使用collect的另一个重载方法:

R collect(Collector collector)

注意到Collector其实是上面supplier、accumulator、combiner的聚合体。那么上面代码就变成:

List list = Stream.of(1, 2, 3, 4).filter(p -> p > 2).collect(Collectors.toList());

将结果收集到map中

先定义如下Person对象

class Person{
  public String name;
  public int age;

  Person(String name, int age){
   this.name = name;
   this.age = age;
  }

  @Override
  public String toString(){
   return String.format("Person{name='%s', age=%d}", name, age);
  }
 }

假设你有一个Stream对象,希望将其中元素收集到一个map中,这样就可以根据他的名称来查找对应年龄,例如:

Map result = people.collect(HashMap::new,(map,p)->map.put(p.name,p.age),Map::putAll); /*使用Collectors.toMap形式*/

Map result = people.collect(Collectors.toMap(p -> p.name, p -> p.age, (exsit, newv) -> newv));

其中Collectors.toMap方法的第三个参数为键值重复处理策略,如果不传入第三个参数,当有相同的键时,会抛出一个IlleageStateException。

或者你想将Person分解为Map存储:

List<Map> personToMap = people.collect(ArrayList::new, (list, p) -> {
  Mapmap = new HashMap<>();
  map.put("name", p.name);
  map.put("age", p.age);
  list.add(map);
}, List::addAll);

分组和分片

对具有相同特性的值进行分组是一个很常见的任务,Collectors提供了一个groupingBy方法,方法签名为:

Collector<T,?,Map> groupingBy(Function classifier, Collector downstream)

classifier:一个获取Stream元素中主键方法。downstream:一个操作对应分组后的结果的方法。

假如要根据年龄来分组:

Map<Integer, List> peopleByAge = people.filter(p -> p.age > 12).collect(Collectors.groupingBy(p -> p.age, Collectors.toList()));

假如我想要根据年龄分组,年龄对应的键值List存储的为Person的姓名,怎么做呢:

Map<Integer, List> peopleByAge = people.collect(Collectors.groupingBy(p -> p.age, Collectors.mapping((Person p) -> p.name, Collectors.toList())));

mapping即为对各组进行投影操作,和Stream的map方法基本一致。

假如要根据姓名分组,获取每个姓名下人的年龄总和(好像需求有些坑爹):

Map sumAgeByName = people.collect(Collectors.groupingBy(p -> p.name, Collectors.reducing(0, (Person p) -> p.age, Integer::sum)));

/* 或者使用summingInt方法 */

sumAgeByName = people.collect(Collectors.groupingBy(p -> p.name, Collectors.summingInt((Person p) -> p.age)));

可以看到Java8的分组功能相当强大,当然你还可以完成更复杂的功能。另外Collectors中还存在一个类似groupingBy的方法:partitioningBy,它们的区别是partitioningBy为键值为Boolean类型的groupingBy,这种情况下它比groupingBy更有效率。

join和统计功能

话说Java8中新增了一个StringJoiner,Collectors的join功能和它基本一样。用于将流中字符串拼接并收集起来,使用很简单:

String names = people.map(p->p.name).collect(Collectors.joining(","))

Collectors分别提供了求平均值averaging、总数couting、最小值minBy、最大值maxBy、求和suming等操作。但是假如你希望将流中结果聚合为一个总和、平均值、最大值、最小值,那么Collectors.summarizing(Int/Long/Double)就是为你准备的,它可以一次行获取前面的所有结果,其返回值为(Int/Long/Double)SummaryStatistics。

DoubleSummaryStatistics dss = people.collect(Collectors.summarizingDouble((Person p)->p.age));
double average=dss.getAverage();
double max=dss.getMax();
double min=dss.getMin();
double sum=dss.getSum();
double count=dss.getCount();

以上这篇JAVA8 stream中三个参数的reduce方法对List进行分组统计操作就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • java8新特性 stream流的方式遍历集合和数组操作

    前言: 在没有接触java8的时候,我们遍历一个集合都是用循环的方式,从第一条数据遍历到最后一条数据,现在思考一个问题,为什么要使用循环,因为要进行遍历,但是遍历不是唯一的方式,遍历是指每一个元素逐一进行处理(目的),而并不是从第一个到最后一个顺次处理的循环,前者是目的,后者是方式. 所以为了让遍历的方式更加优雅,出现了流(stream)! 1.流的目的在于强掉做什么 假设一个案例:将集合A根据条件1过滤为子集B,然后根据条件2过滤为子集C 在没有引入流之前我们的做法可能为: public cl

  • Java8 stream 中利用 groupingBy 进行多字段分组求和案例

    Java8的groupingBy实现集合的分组,类似Mysql的group by分组功能,注意得到的是一个map 对集合按照单个属性分组.分组计数.排序 List<String> items = Arrays.asList("apple", "apple", "banana", "apple", "orange", "banana", "papaya");

  • JAVA JDK8 List分组的实现和用法

    概述 对List进行分组是日常开发中,经常遇到的,在JDK 8中对List按照某个属性分组的代码,超级简单. package test; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.serializer.SerializerFeature; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util

  • java8 集合 多字段 分组 统计个数代码

    1.user实体 package com.demo.dto; public class User { private Integer id; private String userName; private String password; private Integer age; private long c; public User() { super(); // TODO Auto-generated constructor stub } public User(Integer id, S

  • JAVA8 stream中三个参数的reduce方法对List进行分组统计操作

    背景 平时在编写前端代码时,习惯使用lodash来编写'野生'的JavaScript; lodash提供来一套完整的API对JS对象(Array,Object,Collection等)进行操作,这其中就包括_.groupBy 和 _.reduce,即分组和'聚合'(reduce不知道该怎么翻译合适). 使用这些'野生'的API能够极大的提高我本人编写JS代码的效率.而JAVA8开始支持stream和lambda表达式,这些和lodash的API有很多类似的功能.因此我在熟悉lodash的前提下尝

  • Android 中三种启用线程的方法总结

    在多线程编程这块,我们经常要使用Handler(处理),Thread(线程)和Runnable这三个类,那么他们之间的关系你是否弄清楚了呢? 首先说明Android的CPU分配的最小单元是线程,Handler一般是在某个线程里创建的,因而Handler和Thread就是相互绑定的,一一对应. 而Runnable是一个接口,Thread是Runnable的子类.所以说,他俩都算一个进程. HandlerThread顾名思义就是可以处理消息循环的线程,他是一个拥有Looper的线程,可以处理消息循环

  • javascript中函数作为参数调用的方法

    本文实例讲述了javascript中函数作为参数调用的方法.分享给大家供大家参考.具体分析如下: 先来看示例: function Map(){ var obj = {}; this.put = function(key, value){ obj[key] = value; } this.eachMap = function(fn){ for(var attr in obj){ fn(attr, obj[attr]); } } } var m = new Map(); m.put('01', 'a

  • yii2 在控制器中验证请求参数的使用方法

    写api接口时一般会在控制器中简单验证参数的正确性. 使用yii只带验证器(因为比较熟悉)实现有两种方式(效果都不佳). 针对每个请求单独写个 Model , 定义验证规则并进行验证. 缺点:写好多参数验证的 Model 类. 使用 独立验证器 中提到的 $validator->validateValue() 方法直接验证变量值.缺点:写实例化很多验证器对象. 有么有"一劳永逸"的做法,像在 Model 中通过 rules 方法定义验证规则并实现快速验证的呢?有! 使用方法(实现

  • js中获取URL参数的共用方法getRequest()方法实例详解

    下面通过一段代码给大家介绍js中获取URL参数的共用方法getRequest()方法,具体代码如下所示: getRequest : function() { var url = location.search; //获取url中"?"符后的字串 var theRequest = new Object(); if (url.indexOf("?") != -1) { var str = url.substr(1); strs = str.split("&am

  • js获取地址栏中传递的参数(两种方法)

    第一种:字符串拆分法 window.location.href 或者 location.href 或者 window.location 获得地址栏中的所有内容 decodeURI()可以解码地址栏中的数据 恢复中文数据 window.search 获得地址栏中问号及问号之后的数据 //获取地址栏里(URL)传递的参数 function GetRequest(value) { //url例子:www.bicycle.com?id="123456"&Name="bicyc

  • java8 stream中Collectors.toMap空指针问题及解决

    目录 Collectors.toMap空指针问题 Collectors.toMap的坑 Collectors.toMap空指针问题 在工作中遇到了一个List转Map的时候的一个NullPointException. 情形很简单,问题出在Collectors.toMap,当key值冲突的时候理论上会按照我们的代码来替换value,但是这里有个小坑 list.stream().collect(Collectors.toMap(it -> it.getCategoryId(), it -> it.

  • Shell脚本中判断输入参数个数的方法

    $#代表了命令行的参数数量,可以看以下实例: 复制代码 代码如下: if [ $# != 1 ] ; then echo "USAGE: $0 TABNAME" echo " e.g.: $0 CDR_CALL_20040701" exit 1; fi 位置参数 $1, $2,..., $N,$#代表了命令行的参数数量, $0代表了脚本的名字 -ne    不等于 ----------------------- shell 编程中使用到得if语句内判断参数 –b 当

  • Java 8中如何获取参数名称的方法示例

    前言 在Java 8之前的版本,代码编译为class文件后,方法参数的类型是固定的,但参数名称却丢失了,这和动态语言严重依赖参数名称形成了鲜明对比.现在,Java 8开始在class文件中保留参数名,给反射带来了极大的便利. 示例: public class GetRuntimeParameterName { public void createUser(String name, int age, int version) { // } public static void main(Strin

随机推荐