本教程演示了如何使用Java Stream API高效地聚合一个包含日期范围和分组信息的对象列表。通过Collectors.groupingBy按指定属性分组,并从每个组中提取最早的开始日期和最晚的结束日期,从而生成精简的汇总数据。此方法适用于需要基于共同标识符合并数据记录的场景。
问题描述
在数据处理中,我们经常会遇到需要根据某个共同属性对数据记录进行分组和汇总的场景。例如,给定一个entities对象列表,每个对象包含一个开始日期(start_dt)、一个结束日期(stop_dt)和一个组编号(groupnum)。如果多个entities对象共享相同的groupnum,它们就属于同一个逻辑组。我们的目标是聚合这些对象,为每个组生成一个新的entities对象,其中新对象的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
Java 8引入的Stream API为处理集合数据提供了强大而灵活的工具。我们可以利用Collectors.groupingBy方法进行分组,然后对每个组进行进一步的处理以提取所需的聚合信息。
实体类定义
首先,我们需要定义Entities类,它包含start_dt、stop_dt和groupNum字段,以及相应的构造函数、getter方法和toString方法,以便于创建实例和打印输出。
立即学习“Java免费学习笔记(深入)”;
import java.text.ParseException; import java.text.SimpledateFormat; import java.util.Date; import java.util.Objects; public class Entities { private final Date start_dt; private final Date stop_dt; private final int groupNum; // 构造函数,接受字符串日期并解析 public Entities(String start_dt_str, String stop_dt_str, int groupNum) throws ParseException { SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd"); this.start_dt = formatter.parse(start_dt_str); this.stop_dt = formatter.parse(stop_dt_str); this.groupNum = 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; } // Getter方法 public Date getStart_dt() { return start_dt; } public Date getStop_dt() { return stop_dt; } public int getGroupNum() { return groupNum; } @Override public String toString() { SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd"); return "Entities [start_dt=" + formatter.format(start_dt) + ", stop_dt=" + formatter.format(stop_dt) + ", groupNum=" + groupNum + "]"; } @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); } }
核心聚合逻辑
聚合过程主要分为以下几个步骤:
- 创建初始列表:首先,我们需要一个包含所有原始Entities对象的列表。
- 按groupNum分组:使用Collectors.groupingBy(Entities::getGroupNum)将列表中的Entities对象按groupNum分组,结果是一个map<Integer, List<Entities>>。
- 遍历分组并聚合:对Map的每个条目(Entry)进行处理。每个条目代表一个组,其键是groupNum,值是该组中所有Entities对象的列表。
- 提取最早和最晚日期:对于每个组的Entities列表,我们假定列表中的元素已经按日期顺序排列(即,第一个元素的start_dt最早,最后一个元素的stop_dt最晚)。因此,可以直接获取列表的第一个元素的start_dt作为聚合后的start_dt,以及最后一个元素的stop_dt作为聚合后的stop_dt。
- 构建新的Entities对象:使用提取到的最早start_dt、最晚stop_dt和当前的groupNum创建一个新的Entities对象。
- 收集结果:将所有新创建的Entities对象收集到一个新的列表中。
import java.text.ParseException; import java.util.List; import java.util.stream.Collectors; public class AggregationExample { public static void main(String[] args) throws ParseException { // 1. 创建初始列表 List<Entities> baseList = List.of( new Entities("2018-11-13", "2019-01-13", 1), new Entities("2019-01-14", "2019-03-06", 1), new Entities("2019-03-07", "2019-11-18", 1), new Entities("2020-08-23", "2020-08-23", 2), new Entities("2021-11-19", "2022-12-23", 2)); // 2. 使用Stream API进行聚合 List<Entities> result = baseList.stream() // 2.1. 按groupNum分组,结果为Map<Integer, List<Entities>> .collect(Collectors.groupingBy(Entities::getGroupNum)) // 2.2. 将Map转换为Stream<Map.Entry<Integer, List<Entities>>> .entrySet().stream() // 2.3. 对每个分组进行映射,生成新的Entities对象 .map(entry -> { List<Entities> groupEntities = entry.getValue(); // 2.4. 提取最早的start_dt(列表第一个元素的start_dt) // 2.5. 提取最晚的stop_dt(列表最后一个元素的stop_dt) // 注意:这里假设groupEntities列表内部已按日期排序 return new Entities( groupEntities.get(0).getStart_dt(), // 获取组内第一个元素的开始日期 groupEntities.get(groupEntities.size() - 1).getStop_dt(), // 获取组内最后一个元素的结束日期 entry.getKey() // 获取当前组的groupNum ); }) // 2.6. 将结果收集为List<Entities> .toList(); // Java 16+,等同于.collect(Collectors.toList()) // 打印聚合结果 result.forEach(System.out::println); } }
输出:
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]
注意事项
-
输入列表的排序假设: 上述解决方案中,map操作直接通过groupEntities.get(0).getStart_dt()和groupEntities.get(groupEntities.size() – 1).getStop_dt()来获取最早和最晚日期。这强烈依赖于原始baseList在分组前已经按照groupNum和日期(start_dt或stop_dt)进行了排序。如果baseList的元素顺序是随机的,get(0)和get(size – 1)可能无法正确地提供最早和最晚的日期。
更健壮的方法:如果无法保证输入列表的排序,应在map操作内部使用min和max收集器来查找日期:
.map(entry -> { List<Entities> groupEntities = entry.getValue(); Date minStartDate = groupEntities.stream() .map(Entities::getStart_dt) .min(Date::compareTo) .orElse(null); // 处理空组情况 Date maxStopDate = groupEntities.stream() .map(Entities::getStop_dt) .max(Date::compareTo) .orElse(null); // 处理空组情况 return new Entities(minStartDate, maxStopDate, entry.getKey()); })
这种方式虽然代码量稍多,但更加健壮,不依赖于原始列表的内部排序。
-
日期处理: 示例中使用了java.util.Date和java.text.SimpleDateFormat进行日期解析和格式化。java.util.Date是可变对象,且设计上存在一些问题。在现代Java应用中,强烈推荐使用java.time包(即JSR 310)中的LocalDate、LocalDateTime等类,它们提供了更好的API设计、线程安全性和不可变性。
-
异常处理: 在Entities的构造函数中,SimpleDateFormat.parse()方法会抛出ParseException。在实际应用中,应妥善处理此异常,例如通过try-catch块捕获并记录错误,或将其转换为运行时异常。
总结
通过Java Stream API,我们可以以声明式的方式优雅地解决基于共享属性进行数据分组和聚合的问题。Collectors.groupingBy是实现这一目标的关键,它将复杂的数据转换过程分解为易于理解和维护的步骤。在实际应用中,务必考虑数据源的特性(如是否已排序)以及选择合适的日期处理API,以确保解决方案的健壮性和准确性。
评论(已关闭)
评论已关闭