TreeSet无法去重问题排查

1,148 阅读1分钟

背景

最近有个需求,需要记录单据的状态轨迹,为了保证状态轨迹唯一且按时间排序,代码中使用的TreeSet来实现。某次偶然情况,发现出现两条相同的状态轨迹。

业务代码

状态节点

@EqualsAndHashCode(of = "status")
@Data
@AllArgsConstructor
public static class Node implements Comparable<Node>{

    /**
         * 节点状态
         */
    private String status;

    /**
         * 节点时间
         */
    private Date date;

    @Override
    public int compareTo(@NotNull Node o) {
        // 状态相同,证明是Node是相等的
        if (this.status.equals(o.status)) {
            return 0;
        }
        // 状态不同,比较时间
        return this.date.compareTo(o.getDate());
    }
}

TreeSet去重

    public static void main(String[] args) throws InterruptedException {
        Node node1 = new Node("ACCEPT", new Date(1643164565275L));
        Node node2 = new Node("DISPATCH_CAR", new Date(1643167154199L));
        Node node3 = new Node("DELIVERY", new Date(1643167155111L));

        TreeSet<Node> treeSet = new TreeSet<>(Lists.newArrayList(node1, node2, node3));

        Node node4 = new Node("ACCEPT", new Date());
        treeSet.add(node4);

        System.out.println(treeSet);
    }

如上代码所示,Node这个类是使用status字段来判断相等的,排序是使用了date字段。这个时候在使用TreeSet时,期望是能够去重的,但是上述代码实际上出现了两个status=ACCEPT的节点。

原因

TreeSet使用TreeMap来实现,如下为TreeMap#put代码。TreeMap直接使用compareTo方法来比较大小,并且因为是Tree,所以不会每个节点都比较,而是只会比较左右子树的其中一部分。 而业务中,我们使用status做相同的判断,而compareTo则使用date来排序,所以就出现了重复状态节点的情况。

public V put(K key, V value) {
    // 其他省略
    if (key == null)
        throw new NullPointerException();
    @SuppressWarnings("unchecked")
    Comparable<? super K> k = (Comparable<? super K>) key;
    do {
        parent = t;
        cmp = k.compareTo(t.key);
        if (cmp < 0)
            t = t.left;
        else if (cmp > 0)
            t = t.right;
        else
            return t.setValue(value);
    } while (t != null);
    // 其他省略
}

解决方案

使用HashSet去重,最后转成List,再对List进行排序。