在后端开发中,Stream
流确实是一个非常强大且方便的工具,特别是在处理集合(如列表、集合、映射等)时。Stream
提供了一种声明式的方式来处理数据,使得代码更加简洁和易读。
场景
在开发中,我们经常会需要筛选,尤其是当某个属性相同时,我们只需要找到的第一个即可,后面再遇到相同的属性,我们则忽视。因此,我们需要用到
Collectors.toMap()
的合并函数(merge function),用于处理当 Map 的 key 发生冲突时(即多个元素映射到同一个 key),决定保留哪一个 value。
1. 基本行为
-
参数:
-
existing
:Map 中已存在的 value(第一次出现的记录) -
replacement
:当前正在处理的 value(后续出现的记录)
-
-
返回值:决定保留哪一个 value
-
当前逻辑:
-> existing
表示 总是保留已存在的记录,忽略新来的记录。
2. 适用场景
-
输入数据已预排序(如按时间降序)
假设集合 已经按时间降序排列,那么第一个遇到的记录就是最新的,后续相同 key 的记录可以直接丢弃。 -
需要首次出现的记录
如果业务逻辑要求保留第一次出现的值(例如首次检测到的事件)。
3. 常见变体
根据业务需求,合并函数可以灵活调整:
合并策略 | 代码 | 适用场景 |
---|---|---|
保留首次记录 | (existing, replacement) -> existing | 保留首次出现值 |
保留最新记录 | (old, new) -> new.getTime() > old.getTime() ? new : old | 需要时间最新的数据 |
保留最后一次出现的值 | (old, new) -> new | 覆盖旧值 |
合并值 | (old, new) -> old.merge(new) | 需要聚合数据(如累加统计) |
举例1
我需要对一个事件列表(allEvents
)进行处理,主要功能是:按设备名称和标签值的组合作为唯一键,筛选出每个组合下的最新事件记录
先献上代码
return allEvents.stream()
.collect(Collectors.toMap(
event -> String.format("%s|%s",
event.getDeviceName(),
getLabelValue(event, labelType)),
Function.identity(),
(existing, replacement) -> existing
))
.values()
.stream()
.collect(Collectors.toList());
参数解析:
参数位置 | 功能 | 示例/说明 | ||
---|---|---|---|---|
第一个Lambdaevent -> String.format(...) | 生成Map的Key (分组依据) | 用`设备名称 | 标签值作为复合键例如: "摄像头01 | 人员入侵"` | ||
第二个参数Function.identity() | 生成Map的Value | 直接使用事件对象本身作为值 | ||
第三个Lambda(existing, replacement) -> existing | 合并策略 | 当Key冲突时,保留已存在的记录 (由于输入数据已按时间排序,这里保留较早出现的记录) |
关键设计:
-
复合键设计:通过
设备名称+标签值
确保唯一性 -
去重逻辑:利用Map的Key不可重复特性实现去重
.values()
提取结果
-
作用:从
Map<String, Event>
中提取所有Value(即筛选后的事件对象) -
输出:
Collection<Event>
二次流转换
.stream()
.collect(Collectors.toList())
-
作用:将Collection重新转为Stream,最终收集为
List<Event>
-
结果:包含所有唯一键对应的最新事件记录
执行流程图解
graph TD
A[原始列表 allEvents] --> B[Stream流]
B --> C[按 设备|标签 分组]
C --> D{Map操作}
D --> E[Key: 设备A|标签1 → Value: 事件1]
D --> F[Key: 设备A|标签2 → Value: 事件2]
D --> G[Key: 设备B|标签1 → Value: 事件3]
E & F & G --> H[提取所有Values]
H --> I[转换为List]
假设原始数据:
[
{deviceName: "Cam1", labelValue: "入侵", checkTime: "2023-01-02"},
{deviceName: "Cam1", labelValue: "入侵", checkTime: "2023-01-01"}, // 相同设备相同标签
{deviceName: "Cam1", labelValue: "火警", checkTime: "2023-01-03"},
{deviceName: "Cam2", labelValue: "入侵", checkTime: "2023-01-02"}
]
处理结果:
[
{deviceName: "Cam1", labelValue: "入侵", checkTime: "2023-01-02"}, // 保留最新
{deviceName: "Cam1", labelValue: "火警", checkTime: "2023-01-03"},
{deviceName: "Cam2", labelValue: "入侵", checkTime: "2023-01-02"}
]
通过这种处理,可以高效地获取每个设备下每种标签类型的最新事件记录。
举例2 保留最新记录(基于时间字段)
场景:获取每个设备的最新告警记录
public class Test1 {
public static void main(String[] args) {
List<DeviceAlert> alerts = Arrays.asList(
new DeviceAlert("Device1", "2023-01-01 10:00", "高温告警"),
new DeviceAlert("Device1", "2023-01-01 11:00", "高温告警"), // 更晚的时间
new DeviceAlert("Device2", "2023-01-01 09:00", "断电告警")
);
Map<String, DeviceAlert> latestAlerts = alerts.stream()
.collect(Collectors.toMap(
DeviceAlert::getDeviceId, // 按设备ID分组
Function.identity(),
(old,newAlert) -> newAlert.getTime().compareTo(old.getTime()) > 0 ? newAlert : old
));
System.out.println(latestAlerts);
}
}
@Data
@AllArgsConstructor
public class DeviceAlert {
private String deviceId;
private String time;
private String type;
}
举例3 保留最后一次出现的值(直接覆盖)
场景:用户配置的优先级覆盖
@Data
@AllArgsConstructor
public class USerConfig {
private String key;
private String value;
}
public class Test2 {
public static void main(String[] args) {
List<USerConfig> configs = Arrays.asList(
new USerConfig("theme", "dark"),
new USerConfig("theme", "light"), // 最后出现的配置
new USerConfig("language", "zh")
);
Map<String, String> finalConfigs = configs.stream()
.collect(Collectors.toMap(
USerConfig::getKey,
USerConfig::getValue,
(old, newConfig) -> newConfig // 直接覆盖旧值
));
System.out.println(finalConfigs);
}
}
举例4 合并值(聚合数据)
场景:统计每个商品的销售总量
@Data
@AllArgsConstructor
public class OrderItem {
private String product;
private int quantity;
}
public class Test3 {
public static void main(String[] args) {
List<OrderItem> orders = Arrays.asList(
new OrderItem("iPhone", 2),
new OrderItem("iPad", 1),
new OrderItem("iPhone", 3) // 同商品再次出现
);
Map<String, Integer> totalSales = orders.stream()
.collect(Collectors.toMap(
OrderItem::getProduct,
OrderItem::getQuantity,
(oldQty, newQty) -> oldQty + newQty // 数量累加
));
totalSales.forEach((product, quantity) -> System.out.println(product + " → " + quantity));
}
}