玩转stream流之(existing, replacement) -> existing

在后端开发中,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());
参数解析:
参数位置功能示例/说明
第一个Lambda
event -> 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));
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值