
在数据处理和分析中,根据某个共同属性对数据进行分组和聚合是一项非常常见的操作。例如,我们可能需要从一系列事件记录中,按项目id聚合所有相关事件,并计算每个项目的总持续时间,或者找到最早的开始时间和最晚的结束时间。java 8 引入的 stream api 极大地简化了这类操作,提供了声明式和高效的数据处理方式。
假设我们有一个 Entities 列表,每个 Entities 对象包含一个开始日期(start_dt)、一个结束日期(stop_dt)和一个组号(groupNum)。具有相同 groupNum 的实体属于同一个逻辑组。我们的目标是聚合这些实体,对于每个组,我们希望得到一个代表该组的新实体,其 start_dt 是该组中所有实体的最早开始日期,而 stop_dt 则是该组中所有实体的最晚结束日期。
以下是原始数据示例:
| Start | Stop | GroupNum |
|---|---|---|
| 2018-11-13 | 2019-01-13 | 1 |
| 2019-01-14 | 2019-03-06 | 1 |
| 2019-03-07 | 2019-11-18 | 1 |
| 2020-08-23 | 2020-08-23 | 2 |
| 2021-11-19 | 2022-12-23 | 2 |
期望的聚合结果如下:
| Start | Stop | GroupNum |
|---|---|---|
| 2018-11-13 | 2019-11-18 | 1 |
| 2020-08-23 | 2022-12-23 | 2 |
Java Stream API 提供了 Collectors.groupingBy 方法,可以非常方便地将数据流按指定属性分组。结合 map 操作,我们可以进一步处理每个分组,以提取所需的聚合信息。
立即学习“Java免费学习笔记(深入)”;
首先,我们需要定义 Entities 类,并包含必要的字段、构造函数、getter 方法以及 toString 方法,以便于创建和打印对象。为了方便示例,我们还添加了日期字符串解析的构造函数。
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
public class EntityAggregator {
// 定义Entities类
public static class Entities {
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");
private Date start_dt;
private Date stop_dt;
private int groupNum;
// 构造函数:接受Date对象
public Entities(Date start_dt, Date stop_dt, int groupNum) {
this.start_dt = start_dt;
this.stop_dt = stop_dt;
this.groupNum = groupNum;
}
// 构造函数:接受日期字符串,并进行解析
public Entities(String start_dt_str, String stop_dt_str, int groupNum) throws ParseException {
this.start_dt = DATE_FORMAT.parse(start_dt_str);
this.stop_dt = DATE_FORMAT.parse(stop_dt_str);
this.groupNum = groupNum;
}
// Getter 方法
public Date getStart_dt() {
return start_dt;
}
public Date getStop_dt() {
return stop_dt;
}
public int getGroupNum() {
return groupNum;
}
// toString 方法,便于打印
@Override
public String toString() {
return "Entities [start_dt=" + DATE_FORMAT.format(start_dt) +
", stop_dt=" + DATE_FORMAT.format(stop_dt) +
", groupNum=" + groupNum + "]";
}
// 重写 equals 和 hashCode 方法(可选,但推荐)
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Entities entities = (Entities) o;
return groupNum == entities.groupNum &&
Objects.equals(start_dt, entities.start_dt) &&
Objects.equals(stop_dt, entities.stop_dt);
}
@Override
public int hashCode() {
return Objects.hash(start_dt, stop_dt, groupNum);
}
}
public static void main(String[] args) throws ParseException {
// 准备原始数据
List<Entities> baseList = new ArrayList<>();
baseList.add(new Entities("2018-11-13", "2019-01-13", 1));
baseList.add(new Entities("2019-01-14", "2019-03-06", 1));
baseList.add(new Entities("2019-03-07", "2019-11-18", 1));
baseList.add(new Entities("2020-08-23", "2020-08-23", 2));
baseList.add(new Entities("2021-11-19", "2022-12-23", 2));
// 使用 Stream API 进行聚合
List<Entities> result = baseList.stream()
// 1. 按 groupNum 分组
.collect(Collectors.groupingBy(Entities::getGroupNum))
// 2. 将 Map 的 EntrySet 转换为 Stream
.entrySet().stream()
// 3. 对每个分组进行处理,生成新的 Entities 对象
.map(entry -> {
List<Entities> groupEntities = entry.getValue();
// 获取组内第一个元素的 start_dt (假设数据已按日期排序)
Date firstStartDate = groupEntities.get(0).getStart_dt();
// 获取组内最后一个元素的 stop_dt (假设数据已按日期排序)
Date lastStopDate = groupEntities.get(groupEntities.size() - 1).getStop_dt();
// 获取组号
int groupNum = entry.getKey();
// 创建新的聚合实体
return new Entities(firstStartDate, lastStopDate, groupNum);
})
// 4. 将结果收集为 List
.toList(); // Java 16+ 使用 toList(), 之前版本使用 Collectors.toList()
// 打印聚合结果
result.forEach(System.out::println);
}
}执行上述 main 方法,将得到如下输出:
Entities [start_dt=2018-11-13, stop_dt=2019-11-18, groupNum=1] Entities [start_dt=2020-08-23, stop_dt=2022-12-23, groupNum=2]
这与我们期望的聚合结果完全一致。
示例中使用了 java.util.Date 和 java.text.SimpleDateFormat。在现代 Java 开发中(Java 8 及更高版本),强烈推荐使用 java.time 包中的日期时间 API,如 LocalDate、LocalDateTime 等。它们提供了更好的可读性、不可变性、线程安全性以及更强大的功能。
如果使用 java.time,Entities 类可以修改为:
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Objects;
public static class Entities {
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private LocalDate start_dt;
private LocalDate stop_dt;
private int groupNum;
public Entities(LocalDate start_dt, LocalDate stop_dt, int groupNum) {
this.start_dt = start_dt;
this.stop_dt = stop_dt;
this.groupNum = groupNum;
}
public Entities(String start_dt_str, String stop_dt_str, int groupNum) {
this.start_dt = LocalDate.parse(start_dt_str, DATE_FORMATTER);
this.stop_dt = LocalDate.parse(stop_dt_str, DATE_FORMATTER);
this.groupNum = groupNum;
}
// Getters
public LocalDate getStart_dt() { return start_dt; }
public LocalDate getStop_dt() { return stop_dt; }
public int getGroupNum() { return groupNum; }
@Override
public String toString() {
return "Entities [start_dt=" + start_dt.format(DATE_FORMATTER) +
", stop_dt=" + stop_dt.format(DATE_FORMATTER) +
", groupNum=" + groupNum + "]";
}
// equals/hashCode 类似
}相应的,在 main 方法中,比较日期时可以直接使用 LocalDate 的 compareTo 方法。
当前解决方案依赖于每个组内的 Entities 列表已经按日期排序的假设。如果原始 baseList 中的元素顺序是随机的,那么 groupEntities.get(0).getStart_dt() 和 groupEntities.get(groupEntities.size() - 1).getStop_dt() 将无法正确获取最早和最晚日期。
为了更健壮地处理这种情况,应该在 map 阶段明确地找到每个组内的最小 start_dt 和最大 stop_dt:
.map(entry -> {
List<Entities> groupEntities = entry.getValue();
// 明确查找组内的最小开始日期
Date minStart = groupEntities.stream()
.map(Entities::getStart_dt)
.min(Date::compareTo) // 或 LocalDate::compareTo
.orElse(null); // 处理空组的情况,这里不会发生
// 明确查找组内的最大结束日期
Date maxStop = groupEntities.stream()
.map(Entities::getStop_dt)
.max(Date::compareTo) // 或 LocalDate::compareTo
.orElse(null);
int groupNum = entry.getKey();
return new Entities(minStart, maxStop, groupNum);
})这种方式虽然代码量稍多,但确保了无论输入列表顺序如何,都能得到正确的结果。
对于更复杂的聚合逻辑,或者希望一步到位地完成分组和聚合,可以结合 groupingBy 的第二个参数,使用 Collectors.reducing 或 Collectors.collectingAndThen。
例如,使用 reducing 来找到每个组的最小 start_dt 和最大 stop_dt(这会更复杂一些,因为需要同时跟踪两个值,通常需要一个自定义的 BinaryOperator):
// 假设Entities有一个合并方法来更新start/stop
// 这种方式更适合单值聚合,对于同时聚合两个独立值,直接在map中处理可能更清晰。
// 或者定义一个更复杂的下游收集器
List<Entities> resultAdvanced = baseList.stream()
.collect(Collectors.groupingBy(Entities::getGroupNum,
Collectors.reducing(
// 初始值:可以是一个默认实体,或者使用Optional
// 更好的方式是使用 collectingAndThen + SummaryStatistics
// 这里为了简化,我们直接在map中处理
(e1, e2) -> {
Date newStart = e1.getStart_dt().before(e2.getStart_dt()) ? e1.getStart_dt() : e2.getStart_dt();
Date newStop = e1.getStop_dt().after(e2.getStop_dt()) ? e1.getStop_dt() : e2.getStop_dt();
return new Entities(newStart, newStop, e1.getGroupNum());
}
)
))
.entrySet().stream()
.map(entry -> entry.getValue().orElse(null)) // reducing 返回 Optional
.filter(Objects::nonNull)
.toList();这种 reducing 的方式需要 Entities 类能够“合并”自身,并且处理 Optional。对于本例,groupingBy 后跟 map 的链式操作更为直观易懂。
通过本教程,我们学习了如何利用 Java Stream API 的 Collectors.groupingBy 方法,结合 map 操作,高效地实现对对象列表的按属性分组聚合。这种模式在处理各种数据聚合需求时非常有用,能够写出简洁、富有表达力的代码。同时,我们也探讨了在处理日期类型和确保聚合逻辑健壮性方面需要
以上就是Java 中按共享值聚合元素并提取首尾属性的教程的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号