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

利用partitioningBy分组

在JDK8中,可以对流进行方便的自定义分块,通常是根据某种过滤条件将流一分为二。
要求传入一个Predicate,会按照满足条件和不满足条件分成两组,得到的结果是Map<Boolean, List>结构,比如我们按是否未成年分成两组

例子1// Person 实体类
@Data
class Person {
    private String uuid;
    private String name;
    private String gender;
    private int age;
 
    public Person(String name, String gender, int age) {
        this.uuid = UUID.randomUUID().toString();
        this.name = name;
        this.gender = gender;
        this.age = age;
    }
}
// List 集合
List<Person> persons = new ArrayList<>();
persons.add(new Person("张三", "男", 27));
persons.add(new Person("李四", "男", 14));
persons.add(new Person("王五", "女", 17));
persons.add(new Person("赵六", "女", 34));
Map<Boolean, List<Person>> personsByAge = persons.stream()
	.collect(Collectors.partitioningBy(p -> p.getAge() > 18));
System.out.println(JSON.toJSONString(personsByAge));
 
// 输出
{
	false: [{
		"age": 14,
		"gender": "男",
		"name": "李四",
		"uuid": "9fc3be98-f676-42a4-9f02-ebdab328103a"
	}, {
		"age": 17,
		"gender": "女",
		"name": "王五",
		"uuid": "3621044d-25a1-4946-a765-57b074f63f26"
	}],
	true: [{
		"age": 27,
		"gender": "男",
		"name": "张三",
		"uuid": "3f87ec59-29a1-4137-b95b-ae755f0e06ca"
	}, {
		"age": 34,
		"gender": "女",
		"name": "赵六",
		"uuid": "04ed8e9f-545b-49f5-a28b-ce0cccd15663"
	}]
}

例子2:有一组人名,包含中文和英文,在 JDK8 中可以通过 partitioningBy 收集器将其区分开来。

// 创建一个包含人名称的流(英文名和中文名)
Stream<String> stream = Stream.of("Alen", "Hebe", "Zebe", "张成瑶", "钟其林");
// 通过判断人名称的首字母是否为英文字母,将其分为两个不同流
final Map<Boolean, List<String>> map = stream.collect(Collectors.partitioningBy(s -> {
    // 如果是英文字母,则将其划分到英文人名,否则划分到中文人名
    int code = s.codePointAt(0);
    return (code >= 65 && code <= 90) || (code >= 97 && code <= 122);
}));
// 输出分组结果
map.forEach((isEnglishName, names) -> {
    if (isEnglishName) {
        System.out.println("英文名称如下:");
    } else {
        System.out.println("中文名称如下:");
    }
    names.forEach(name -> System.out.println("\t" + name));
});
程序输出结果如下:
中文名称如下:
	张成瑶
	钟其林
英文名称如下:
	Alen
	Hebe
	Zebe

Java8的groupingBy实现集合的分组,类似Mysql的group by分组功能,注意得到的是一个map

对集合按照单个属性分组、分组计数、排序

List items =
Arrays.asList(“apple”, “apple”, “banana”,
“apple”, “orange”, “banana”, “papaya”);

// 分组

Map<String, List<String>> result1 = items.stream().collect(
    Collectors.groupingBy(
        Function.identity()
    )
);

{
	papaya = [papaya],
	orange = [orange],
	banana = [banana, banana], 
	apple = [apple, apple, apple]
}

// 分组计数

Map<String, Long> result2 = items.stream().collect(
    Collectors.groupingBy(
        Function.identity(), Collectors.counting()
    )
);
分组结果
{  papaya=1, 
   orange=1, 
   banana=2, 
   apple=3
   }

分组, 计数和排序

Map<String, Long> finalMap = new LinkedHashMap<>();
result2.entrySet().stream()
    .sorted(Map.Entry.<String, Long>comparingByValue().reversed())
    .forEachOrdered(e -> finalMap.put(e.getKey(), e.getValue()));
分组结果:
 {apple=3, banana=2, papaya=1, orange=1}

集合按照多个属性分组
1.多个属性拼接出一个组合属性

public static void main(String[] args) {
  User user1 = new User("zhangsan", "beijing", 10);
  User user2 = new User("zhangsan", "beijing", 20);
  User user3 = new User("lisi", "shanghai", 30);
  List<User> list = new ArrayList<User>();
  list.add(user1);
  list.add(user2);
  list.add(user3);
  Map<String, List<User>> collect = list.stream().collect(Collectors.groupingBy(e -> fetchGroupKey(e)));
  //{zhangsan#beijing=[User{age=10, name='zhangsan', address='beijing'}, User{age=20, name='zhangsan', address='beijing'}], 
  // lisi#shanghai=[User{age=30, name='lisi', address='shanghai'}]}
  System.out.println(collect);
} 
 
private static String fetchGroupKey(User user){
  return user.getName() +"#"+ user.getAddress();
}

2.嵌套调用groupBy

User user1 = new User("zhangsan", "beijing", 10);
User user2 = new User("zhangsan", "beijing", 20);
User user3 = new User("lisi", "shanghai", 30);
List<User> list = new ArrayList<User>();
list.add(user1);
list.add(user2);
list.add(user3);
Map<String, Map<String, List<User>>> collect
    = list.stream().collect(
        Collectors.groupingBy(
            User::getAddress, Collectors.groupingBy(User::getName)
        )
);
System.out.println(collect);
  1. 使用Arrays.asList

我有一个与Web访问记录相关的域对象列表。这些域对象可以扩展到数千个。
我没有资源或需求将它们以原始格式存储在数据库中,因此我希望预先计算聚合并将聚合的数据放在数据库中。
我需要聚合在5分钟窗口中传输的总字节数,如下面的sql查询

select 
 round(request_timestamp, '5') as window, --round timestamp to the nearest 5 minute
 cdn, 
 isp, 
 http_result_code, 
 transaction_time, 
 sum(bytes_transferred)
from web_records
group by 
  round(request_timestamp, '5'), 
  cdn, 
  isp, 
  http_result_code, 
  transaction_time

在java 8中,我当前的第一次尝试是这样的,我知道这个解决方案类似于Group by multiple field names in java 8

Map<Date, Map<String, Map<String, Map<String, Map<String, Integer>>>>>>> aggregatedData =
webRecords
  .stream()
  .collect(Collectors.groupingBy(WebRecord::getFiveMinuteWindow,
        Collectors.groupingBy(WebRecord::getCdn,
         Collectors.groupingBy(WebRecord::getIsp,
          Collectors.groupingBy(WebRecord::getResultCode,
            Collectors.groupingBy(WebRecord::getTxnTime,
             Collectors.reducing(0,
                       WebRecord::getReqBytes(),
                       Integer::sum)))))));
                       
这是可行的,但它是丑陋的,所有这些嵌套的地图是一个噩梦!
要将地图“展平”或“展开”成行,我必须这样做

for (Date window : aggregatedData.keySet()) {
 for (String cdn : aggregatedData.get(window).keySet()) {
  for (String isp : aggregatedData.get(window).get(cdn).keySet()) {
   for (String resultCode : aggregatedData.get(window).get(cdn).get(isp).keySet()) {
    for (String txnTime : aggregatedData.get(window).get(cdn).get(isp).get(resultCode).keySet()) {
 
      Integer bytesTransferred = aggregatedData.get(window).get(cdn).get(distId).get(isp).get(resultCode).get(txnTime);
      AggregatedRow row = new AggregatedRow(window, cdn, distId...

如你所见,这是相当混乱和难以维持。

有谁知道更好的方法吗?任何帮助都将不胜感激。

我想知道是否有更好的方法来展开嵌套的映射,或者是否有一个库允许您对集合进行分组。

最佳答案

您应该为地图创建自定义密钥。最简单的方法是使用Arrays.asList:

Function<WebRecord, List<Object>> keyExtractor = wr ->
  Arrays.<Object>asList(wr.getFiveMinuteWindow(), wr.getCdn(), wr.getIsp(),
       wr.getResultCode(), wr.getTxnTime());
 
Map<List<Object>, Integer> aggregatedData = webRecords.stream().collect(
   Collectors.groupingBy(keyExtractor, Collectors.summingInt(WebRecord::getReqBytes)));

在这种情况下,键是按固定顺序列出的5个元素。不是很面向对象,但很简单。或者,您可以定义自己的表示自定义键的类型,并创建适当的hashCode/equals实现。


近期的项目里,遇到一个需求:对于含有多个元素的List,按照其中的某几个属性进行分组,比如Persion::getAge、Persion::getType、Persion::getGender等字段。下面就让我们讨论一下如何比较优雅的按多字段进行分组groupingBy。

利用Stream进行分组

Stream是Java8的一个新特性,主要用户集合数据的处理,如排序、过滤、去重等等功能,这里我们不展开讲解。本文主要讲解的是利用Stream.collect()来对List进行分组。

Person类Person.java:

public class Person {
    /**
     * id
     */
    private Integer id;
    /**
     * 年龄
     */
    private Integer age;
    /**
     * 类型
     */
    private String type;
    /**
     * 姓名
     */
    private String name;
    /**
     * 性别
     */
    private String gender;
    public Integer getId() {
        return id;
    }
    public Person setId(Integer id) {
        this.id = id;
        return this;
    }
    public Integer getAge() {
        return age;
    }
    public Person setAge(Integer age) {
        this.age = age;
        return this;
    }
    public String getType() {
        return type;
    }
    public Person setType(String type) {
        this.type = type;
        return this;
    }
    public String getName() {
        return name;
    }
    public Person setName(String name) {
        this.name = name;
        return this;
    }
    public String getGender() {
        return gender;
    }
    public Person setGender(String gender) {
        this.gender = gender;
        return this;
    }
}

1. 利用单个字段进行分组

如上面的Person类,如果对于其中的某一个字段进行分组(如gender),则比较简单,我们可以利用Stream.collect()和Collectors.groupingBy结合,即可进行分组groupingBy,代码如下:

public class TestGroupingBy {
    public static void main(String[] args) {
        List<Person> personList = Arrays.asList(
                new Person().setId(1).setAge(18).setType("student").setName("user - 1").setGender("male"),
                new Person().setId(2).setAge(20).setType("student").setName("user - 2").setGender("male"),
                new Person().setId(3).setAge(18).setType("student").setName("user - 3").setGender("male"),
                new Person().setId(4).setAge(18).setType("student").setName("user - 4").setGender("male"),
                new Person().setId(5).setAge(35).setType("teacher").setName("user - 5").setGender("male"),
                new Person().setId(6).setAge(35).setType("teacher").setName("user - 6").setGender("male"),
                new Person().setId(7).setAge(20).setType("student").setName("user - 7").setGender("male"),
                new Person().setId(8).setAge(20).setType("student").setName("user - 8").setGender("female"),
                new Person().setId(9).setAge(20).setType("student").setName("user - 9").setGender("female"),
                new Person().setId(10).setAge(20).setType("student").setName("user - 10").setGender("female")
        );
        Map<String, List<Person>> groupingMap = personList.stream().collect(Collectors.groupingBy(Person::getGender));
}

其中的groupingMap ,类型为Map<String, List>,第一个泛型为String即分组字段(本例中为gender字段)的类型,第二个泛型为List及分组结果的类型。

我们在Debug模式下运行代码,可以看到groupingMap 数据如下:
在这里插入图片描述
可以看到personList数据按照gender属性被分成了两组。

2. 利用多个字段进行分组

上面的例子是按单个字段分组,如果需要按照多个字段,如gender、age、type三个字段进行分组,同样也可以可以利用Stream.collect()和Collectors.groupingBy结合的方式进行分组,不过该方式中调用Collectors.groupingBy时需要多次嵌套调用,测试代码如下:

public class TestGroupingBy {
    public static void main(String[] args) {
        List<Person> personList = Arrays.asList(
                new Person().setId(1).setAge(18).setType("student").setName("user - 1").setGender("male"),
                new Person().setId(2).setAge(20).setType("student").setName("user - 2").setGender("male"),
                new Person().setId(3).setAge(18).setType("student").setName("user - 3").setGender("male"),
                new Person().setId(4).setAge(18).setType("student").setName("user - 4").setGender("male"),
                new Person().setId(5).setAge(35).setType("teacher").setName("user - 5").setGender("male"),
                new Person().setId(6).setAge(35).setType("teacher").setName("user - 6").setGender("male"),
                new Person().setId(7).setAge(20).setType("student").setName("user - 7").setGender("male"),
                new Person().setId(8).setAge(20).setType("student").setName("user - 8").setGender("female"),
                new Person().setId(9).setAge(20).setType("student").setName("user - 9").setGender("female"),
                new Person().setId(10).setAge(20).setType("student").setName("user - 10").setGender("female")
        );
        // 多字段嵌套分组
        Map<String, Map<Integer, Map<String, List<Person>>>> groupingMap = personList.stream().collect(
                Collectors.groupingBy(Person::getGender, 
                        Collectors.groupingBy(Person::getAge, 
                                Collectors.groupingBy(Person::getType)
                        )
                )
        );
    }
    }

其中groupingMap类型为Map<String, Map<Integer, Map<String, List>>>,是一个嵌套了三层的Map,对应的泛型String/Integer/String分别为对应分组字段的类型,最后一层Map的value类型为List为实际分组后的数据集合类型,为方便查看数据,特意按Json格式贴出数据如下:

{
  "female": {
    "20": {
      "student": [
        {
          "id": 8,
          "age": 20,
          "type": "student",
          "name": "user - 8",
          "gender": "female"
        },
        {
          "id": 9,
          "age": 20,
          "type": "student",
          "name": "user - 9",
          "gender": "female"
        },
        {
          "id": 10,
          "age": 20,
          "type": "student",
          "name": "user - 10",
          "gender": "female"
        }
      ]
    }
  },
  "male": {
    "18": {
      "student": [
        {
          "id": 1,
          "age": 18,
          "type": "student",
          "name": "user - 1",
          "gender": "male"
        },
        {
          "id": 3,
          "age": 18,
          "type": "student",
          "name": "user - 3",
          "gender": "male"
        },
        {
          "id": 4,
          "age": 18,
          "type": "student",
          "name": "user - 4",
          "gender": "male"
        }
      ]
    },
    "20": {
      "student": [
        {
          "id": 2,
          "age": 20,
          "type": "student",
          "name": "user - 2",
          "gender": "male"
        },
        {
          "id": 7,
          "age": 20,
          "type": "student",
          "name": "user - 7",
          "gender": "male"
        }
      ]
    },
    "35": {
      "teacher": [
        {
          "id": 5,
          "age": 35,
          "type": "teacher",
          "name": "user - 5",
          "gender": "male"
        },
        {
          "id": 6,
          "age": 35,
          "type": "teacher",
          "name": "user - 6",
          "gender": "male"
        }
      ]
    }
  }
}

可以看到,原先的List数据,按照gender/age/type三个属性,分成了三层的Map,对于这种多层的Map代码上处理起来会有一些不方便。并且如果分组字段更多的话,所嵌套的Collectors.groupingBy也会更加多,代码书写起来也不太优雅。

下面将介绍另外一种按多字段分组的方法。

3. 利用Collectors.groupingBy与Function结合进行多字段分组

查看Collectors.groupingByAPI会发现,其中一种用法是第一个参数为Function,如下:
在这里插入图片描述
简单翻译一下就是:一种将输入元素映射到键的分类函数。即需要定义一个函数Function,该函数将元素对象映射到一个键的集合里。代码示例如下:

public class TestGroupingBy {
    public static void main(String[] args) {
        List<Person> personList = Arrays.asList(
                new Person().setId(1).setAge(18).setType("student").setName("user - 1").setGender("male"),
                new Person().setId(2).setAge(20).setType("student").setName("user - 2").setGender("male"),
                new Person().setId(3).setAge(18).setType("student").setName("user - 3").setGender("male"),
                new Person().setId(4).setAge(18).setType("student").setName("user - 4").setGender("male"),
                new Person().setId(5).setAge(35).setType("teacher").setName("user - 5").setGender("male"),
                new Person().setId(6).setAge(35).setType("teacher").setName("user - 6").setGender("male"),
                new Person().setId(7).setAge(20).setType("student").setName("user - 7").setGender("male"),
                new Person().setId(8).setAge(20).setType("student").setName("user - 8").setGender("female"),
                new Person().setId(9).setAge(20).setType("student").setName("user - 9").setGender("female"),
                new Person().setId(10).setAge(20).setType("student").setName("user - 10").setGender("female")
        );
        // 定义一个函数Function,该函数将元素对象映射到一个键的集合里
        Function<Person, List<Object>> compositeKey = person ->
                Arrays.asList(person.getGender(), person.getAge(), person.getType());
        // 分组
        Map<List<Object>, List<Person>> groupingMap =
                personList.stream().collect(Collectors.groupingBy(compositeKey, Collectors.toList()));
    }
}

通过在Debug模式下运行代码,可以看到groupingMap的数据结构如下:
在这里插入图片描述
groupingMap数据仅仅只有一层,但是其键值Key却是一个List,里面包含了分组字段的值,如上图中的male、35、teacher是集合中属性gender/age/type分别是male、35、teacher的元素集合。数据按Json格式贴出如下:

{
  "[male, 35, teacher]": [
    {
      "id": 5,
      "age": 35,
      "type": "teacher",
      "name": "user - 5",
      "gender": "male"
    },
    {
      "id": 6,
      "age": 35,
      "type": "teacher",
      "name": "user - 6",
      "gender": "male"
    }
  ],
  "[female, 20, student]": [
    {
      "id": 8,
      "age": 20,
      "type": "student",
      "name": "user - 8",
      "gender": "female"
    },
    {
      "id": 9,
      "age": 20,
      "type": "student",
      "name": "user - 9",
      "gender": "female"
    },
    {
      "id": 10,
      "age": 20,
      "type": "student",
      "name": "user - 10",
      "gender": "female"
    }
  ],
  "[male, 20, student]": [
    {
      "id": 2,
      "age": 20,
      "type": "student",
      "name": "user - 2",
      "gender": "male"
    },
    {
      "id": 7,
      "age": 20,
      "type": "student",
      "name": "user - 7",
      "gender": "male"
    }
  ],
  "[male, 18, student]": [
    {
      "id": 1,
      "age": 18,
      "type": "student",
      "name": "user - 1",
      "gender": "male"
    },
    {
      "id": 3,
      "age": 18,
      "type": "student",
      "name": "user - 3",
      "gender": "male"
    },
    {
      "id": 4,
      "age": 18,
      "type": "student",
      "name": "user - 4",
      "gender": "male"
    }
  ]
}

由于Map只有一层,用该方式分组的结果,对于我们业务也是比较友好,代码里对数据处理起来也是比较方便的。可以看到,从代码书写角度以及分组处理后得到的结果,该方法都是最优雅的

写在最后

可以看到,如果分组字段只有一个,我们可以用比较简单的利用Stream.collect()和Collectors.groupingBy进行处理,但对于多个字段的分组操作,建议还是用Collectors.groupingBy和Function进行处理。

到此这篇关于Java Stream实现多字段分组groupingBy操作详解的文章就介绍到这了,更多相关Java Stream分组内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java 8引入了Stream API,有许多新方法,其有一个对于分组和聚合操作非常有用,那就是groupingBy()方法。它可以将一个流分组成一个Map,其Entry的key是分组的条件,value是分组的结果,通常是一个List或其他集合。groupingBy()方法的另一个形式是groupingByConcurrent(),它返回一个并发Map,对于并发访问更加友好。 利用groupingBy()方法进行字段分组求和操作的示例如下: 假设有一个Person类,其包含属性:name, age和salary。现在我们需要根据name和age两个字段进行分组,并求出每组的salary总和。可以使用groupingBy()方法加上summingDouble()方法来实现: ``` List<Person> persons = Arrays.asList( new Person("Tom", 20, 5000), new Person("Tom", 21, 4000), new Person("Jerry", 22, 6000), new Person("Jerry", 23, 5500), new Person("Kate", 24, 7000), new Person("Kate", 25, 8000) ); Map<String, Map<Integer, Double>> result = persons.stream() .collect(Collectors.groupingBy(Person::getName, Collectors.groupingBy(Person::getAge, Collectors.summingDouble(Person::getSalary)))); ``` 这里的personList是一个包含了6个Person对象的List,我们希望将其相同name和age的对象分组,求得salary的总和。在groupingBy()方法,第一个参数是分组条件,这里是Person::getName,第二个参数是分组的结果,这里是一个嵌套的groupingBy()方法,用于再次按照age进行分组,结果是一个Map<Integer, Double>。最后,我们使用summingDouble()方法对salary字段进行求和,得到各个分组的salary总和。这里的result是一个Map<String, Map<Integer, Double>>类型的对象,其key是name,value是以age为key,salary总和为value的子Map,就是我们需要的结果。 这样,我们就利用Java 8Stream API和groupingBy()方法进行了多字段分组求和操作,代码简洁,可读性强,非常方便。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值