Java 优雅地 Diff 两个 List 来获取新增、修改和删除的数据

Last Modified: 2024/01/09

场景描述

有时候会将数据库中查出的一组数据返回给前端页面进行修改:用户通过界面可以删除一些数据,也可以增加一些数据并修改一些数据,然后将这些修改后的数据重新保存到数据库中。这样说可能有些抽象,可以看下面这张图来更直观的理解。

这张表的左侧是经过用户修改后传递到服务端待更新的数据,右侧是当前数据库中查出的数据。假设数据的唯标识为 id,通过比较可以看出,id 为 2 和 3 的元素左右两侧都有,因此这两条数据是被修改的元素。左侧有一个 id 为空的数据,因此是新增的数据。右侧有一个 id 为 4 的数据,左侧没有,因此是这是被用户删除的数据。

因此我们要执行的数据库操作如下:

  • 删除 id 为 4 的数据;
  • 修改 id 为 2 和 3 的数据;
  • 新增一条数据,并给 id 赋值。

执行这个数据库操作并不难,但是在这之前我们得 Diff 前端页面传递过来的 specDTOList 和 数据库中查出的 specList。Diff 的结果是三个 List,分别用来存储新增、修改和删除的元素。因此我们需要定义一个类来封装 Diff 的结果。

public static class DiffR<T, E> {
    private final List<T> newList;
    private final List<T> modList;
    private final List<E> delList;

    public DiffR(List<T> newList, List<T> modList, List<E> delList) {
        this.newList = newList;
        this.modList = modList;
        this.delList = delList;
    }

    public List<T> getN() {
        return newList;
    }

    public List<T> getM() {
        return modList;
    }

    public List<E> getD() {
        return delList;
    }
}

通过上面的泛型定义可以看出我的用意:我的目标是 Diff 任意两个 List,而不局限于特定类型的两个 List。那么该怎样定义 diff 函数呢?

public static <T, E> DiffR<T, E> diff(List<T> lst1, List<E> lst2);

这样可以吗?显然不可以,因为我们还需要知道根据什么字段来 diff lst1 和 lst2。因此,除了两个待比较的 list 之外,我们还需要明确给出这两个 List 中的元素要根据什么字段来 diff。这需要一点技巧:

public static <T, E, R> DiffR<T, E> diff(List<T> lst1, List<E> lst2, Function<T, R> f1, Function<E, R> f2);

对比之前的函数参数,又多了两个 Function 类型的参数。为什么是 Function?Function<X, Y> 可以看成是 X 到 Y 的映射。拿 f1 来说,就是将 T 映射到 R,通过该函数我们可以将元素映射到字段,这不就达到了指定字段的目的了吗?同理 f2 是用来指定 lst2 中的元素到字段的映射。

有了上面的铺垫,下面让我们来实现这个 diff 函数。

Diff 方法实现

public static <T, E, R> DiffR<T, E> diff(List<T> lst1, List<E> lst2, Function<T, R> f1, Function<E, R> f2) {
    List<T> newList = new ArrayList<>();
    List<T> modList = new ArrayList<>();
    List<E> delList = new ArrayList<>();
    for (T t : lst1) {
        if (f1.apply(t) == null) {
            newList.add(t);
        }
    }
    Map<R, E> lst2Map = lst2.stream().collect(Collectors.toMap(f2, e -> e, (a, b) -> b));
    for (T t : lst1) {
        R k1 = f1.apply(t);
        if (k1 != null && lst2Map.containsKey(k1)) {
            modList.add(t);
        }
    }
    Map<R, T> lst1Map = lst1.stream().collect(Collectors.toMap(f1, e -> e, (a, b) -> b));
    for (E t : lst2) {
        R k2 = f2.apply(t);
        if (k2 != null && !lst1Map.containsKey(k2)) {
            delList.add(t);
        }
    }
    return new DiffR<>(newList, modList, delList);
}

Diff 方法使用实战

假设 Spec 和 SpecDTO 定义如下:

@Data
public class Spec {
    private String id;
    private String name;
}
@Data
public class SpecDTO {
    private String id;
    private String name;
}

现在我们要比较 specDTOList 和 specList,使用方法如下:

Diff<SpecDTO, Spec> r = diff(specDTOList, specList, SpecDTO::getId, Spec::getId);
r.getNewList();
r.getModList();
r.getDelList();

完整代码可参考 CollectionUtil.java

有问题吗?点此反馈!

温馨提示:反馈需要登录