在业务数据处理里,我们经常会碰到这样的需求:有一批按时间顺序排列的记录,每条记录包含所属分组标识、执行状态以及发生时间。状态只有成功和失败两种,现在要算出每个分组内部连续失败所持续的时间长度。所谓连续失败,是指中间不能夹杂任何成功记录,一旦出现了成功,前面的失败时段就结束了,后面再失败要重新计时。

问题拆解与数据模型
我们先明确输入数据的结构。假设有一个简单的记录类,包含分组键、是否成功、以及发生时间。实际场景中,时间可能是字符串或者时间戳,这里统一用 LocalDateTime 方便计算。理解数据模型是写对逻辑的前提,如果时间字段无序,还需要先按分组和时间排序,否则连续性的判断会完全错误。
连续失败时长的计算本质是一个状态机:遍历同一个分组内的记录,维护一个“当前连续失败起始时间”。当遇到失败时,若起始时间为空就记录下来;当遇到成功时,如果起始时间不为空,就用当前成功时间减去起始时间,得到一个连续失败时段,然后清空起始时间。遍历结束后,若某个分组还留有起始时间,说明最后一段失败一直到列表末尾都没有恢复,这部分也要结算。
import java.time.LocalDateTime;
public class Record {
private String groupKey;
private boolean success;
private LocalDateTime time;
public Record(String groupKey, boolean success, LocalDateTime time) {
this.groupKey = groupKey;
this.success = success;
this.time = time;
}
public String getGroupKey() {
return groupKey;
}
public boolean isSuccess() {
return success;
}
public LocalDateTime getTime() {
return time;
}
}
核心算法实现
下面给出完整的计算代码。我们使用一个 Map 来保存每个分组当前正在进行的连续失败起点。为了结果清晰,再准备一个内部类存储每次结算出来的失败时段。注意分组之间互不影响,所以 Map 的 key 就是分组标识。
算法主循环先按分组归并,其实不一定要先分组再遍历,边遍历边用 Map 维护也能达到同样效果,且只需一次循环。代码里我们采用一次循环的方式,这样即使原始列表未预先按分组排序也能正确累计,只是同组内记录必须是时间有序的。若同组内时间乱序,需要提前用 Collections.sort 结合比较器处理。
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.*;
public class FailureDurationCalculator {
// 存储单段连续失败结果
public static class FailSegment {
String groupKey;
LocalDateTime start;
LocalDateTime end;
long seconds;
FailSegment(String groupKey, LocalDateTime start, LocalDateTime end) {
this.groupKey = groupKey;
this.start = start;
this.end = end;
this.seconds = Duration.between(start, end).getSeconds();
}
@Override
public String toString() {
return groupKey + " 连续失败 " + seconds + " 秒 (" + start + " ~ " + end + ")";
}
}
public static List<FailSegment> calculate(List<Record> records) {
// 分组 -> 当前连续失败起点
Map<String, LocalDateTime> runningStart = new HashMap<>();
List<FailSegment> result = new ArrayList<>();
for (Record r : records) {
String key = r.getGroupKey();
if (r.isSuccess()) {
LocalDateTime start = runningStart.get(key);
if (start != null) {
// 成功打断失败,结算前一段
result.add(new FailSegment(key, start, r.getTime()));
runningStart.remove(key);
}
} else {
if (!runningStart.containsKey(key)) {
runningStart.put(key, r.getTime());
}
}
}
// 遍历完仍有未结束的失败段
for (Map.Entry<String, LocalDateTime> entry : runningStart.entrySet()) {
// 用列表最大时间作为结束,这里简单取当前时间演示
LocalDateTime end = LocalDateTime.now();
result.add(new FailSegment(entry.getKey(), entry.getValue(), end));
}
return result;
}
}
使用示例与输出分析
我们构造一组模拟数据来验证。分组 A 先失败两次,然后成功,之后又失败一次并持续到最后;分组 B 只有一次失败且未被成功打断。通过打印结果可以看到,A 被拆成了两段,B 是一段到当前时间为止。
这种实现方式时间复杂度是 O(n),空间复杂度是 O(k),k 为分组数,非常高效。比起先按 groupKey 分组成多个子列表再各自扫描,一次循环更省事,也避免了额外创建集合。如果数据量极大,可以考虑并行流,但并行时 Map 需要换成线程安全结构或采用 groupingBy 后分别处理。
import java.time.LocalDateTime;
import java.util.*;
public class Demo {
public static void main(String[] args) {
List<Record> data = Arrays.asList(
new Record("A", false, LocalDateTime.of(2023,1,1,10,0)),
new Record("A", false, LocalDateTime.of(2023,1,1,10,5)),
new Record("A", true, LocalDateTime.of(2023,1,1,10,10)),
new Record("A", false, LocalDateTime.of(2023,1,1,11,0)),
new Record("B", false, LocalDateTime.of(2023,1,1,9,0))
);
List<FailureDurationCalculator.FailSegment> segments =
FailureDurationCalculator.calculate(data);
for (FailureDurationCalculator.FailSegment s : segments) {
System.out.println(s);
}
}
}
常见误区与改进
不少人在写类似逻辑时会直接用两个嵌套循环:外层遍历分组,内层遍历该组记录。这本身没问题,但容易在“最后一段未结束”的边界上漏算,或者在遇到成功时忘记清空起点导致时长被拉长。使用 Map 集中管理状态能让边界处理更显式。
另一个误区是用毫秒时间戳相减后除以 1000 得到秒,却忽略了时区与夏令时。java.time 的 Duration 已经帮我们处理了这些细节。如果原始数据是字符串时间,建议先用 DateTimeFormatter 解析为 LocalDateTime 再运算,不要自己截取数字做算术。对于跨天的连续失败,Duration 同样适用,无需额外写日期进位逻辑。