You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何基于多属性对Java List分组并聚合关联属性

按多属性分组并聚合Job列表的实现方案

1. 定义数据模型

先明确原始数据的实体类,以及分组后目标的DTO类:

原始实体类(Java 8+)

import java.util.Objects;

public class Person {
    private Integer id;
    private String name;
    private Integer age;
    private String job;

    // 全参构造器、getter方法
    public Person(Integer id, String name, Integer age, String job) {
        this.id = id;
        this.name = name;
        this.age = age;
        this.job = job;
    }

    public Integer getId() { return id; }
    public String getName() { return name; }
    public Integer getAge() { return age; }
    public String getJob() { return job; }

    // 重写equals和hashCode,仅基于id/name/age三个分组字段
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return Objects.equals(id, person.id) && Objects.equals(name, person.name) && Objects.equals(age, person.age);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, name, age);
    }
}

Java 16+ 简化写法(用Record)

如果用Java 16及以上,用Record自动实现equals/hashCode,代码更简洁:

// 原始数据Record
public record Person(Integer id, String name, Integer age, String job) {}

// 分组后的目标DTO Record
public record GroupedPerson(Integer id, String name, Integer age, List<String> jobs) {}

2. 核心分组逻辑

用Stream API的Collectors.groupingBy实现按多属性分组,同时用Collectors.mapping聚合job字段为List:

方式一:直接用原始实体类作为分组键

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class GroupingExample {
    public static void main(String[] args) {
        // 构造原始列表
        List<Person> originalList = List.of(
            new Person(1, "Jane", 25, "job1"),
            new Person(1, "Jane", 25, "job2"),
            new Person(1, "Jane", 25, "job3"),
            new Person(2, "Adam", 30, "job4"),
            new Person(2, "Adam", 30, "job5")
        );

        // 分组:键为Person(仅匹配id/name/age),值为对应的job列表
        Map<Person, List<String>> groupedMap = originalList.stream()
            .collect(Collectors.groupingBy(
                // 生成分组键:忽略job字段,仅保留分组属性
                p -> new Person(p.getId(), p.getName(), p.getAge(), null),
                Collectors.mapping(Person::getJob, Collectors.toList())
            ));

        // 转换为目标DTO列表(如果需要)
        List<GroupedPerson> resultList = groupedMap.entrySet().stream()
            .map(entry -> new GroupedPerson(
                entry.getKey().getId(),
                entry.getKey().getName(),
                entry.getKey().getAge(),
                entry.getValue()
            ))
            .collect(Collectors.toList());

        // 输出验证
        resultList.forEach(gp -> 
            System.out.printf("%d %s %d  (%s)%n", 
                gp.id(), gp.name(), gp.age(), 
                String.join(", ", gp.jobs())
            )
        );
    }
}

方式二:用自定义分组键类(更清晰)

为了避免原始实体类的干扰,可以专门定义一个只包含分组属性的类作为键:

import java.util.Objects;

public class GroupKey {
    private Integer id;
    private String name;
    private Integer age;

    public GroupKey(Integer id, String name, Integer age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    // 重写equals和hashCode
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        GroupKey groupKey = (GroupKey) o;
        return Objects.equals(id, groupKey.id) && Objects.equals(name, groupKey.name) && Objects.equals(age, groupKey.age);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, name, age);
    }

    // getter方法
    public Integer getId() { return id; }
    public String getName() { return name; }
    public Integer getAge() { return age; }
}

对应的分组代码:

Map<GroupKey, List<String>> groupedMap = originalList.stream()
    .collect(Collectors.groupingBy(
        p -> new GroupKey(p.getId(), p.getName(), p.getAge()),
        Collectors.mapping(Person::getJob, Collectors.toList())
    ));

关键注意点

  • 分组键的正确性:必须保证相同id/name/age的对象被识别为同一个键,普通类必须重写equals和hashCode,Record则自动基于所有字段生成(如果用分组专用Record,只包含id/name/age即可)。
  • 聚合逻辑:Collectors.mapping负责提取每个对象的job字段,Collectors.toList()将这些字段收集成List。
  • 结果转换:如果不需要Map结构,直接将Map的entry转换为目标DTO列表即可。

内容的提问来源于stack exchange,提问作者Di_

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 08:10:36