TreeSet Comparator 在某些情况下无法删除重复项?

2024-02-16

我的 TreeSet 有以下比较器:

public class Obj {
    public int id;
    public String value;
    public Obj(int id, String value) {
        this.id = id;
        this.value = value;
    }
    public String toString() {
        return "(" + id + value + ")";
    }
}

Obj obja = new Obj(1, "a");
Obj objb = new Obj(1, "b");
Obj objc = new Obj(2, "c");
Obj objd = new Obj(2, "a");
Set<Obj> set = new TreeSet<>((a, b) -> {
    System.out.println("Comparing " + a + " and " + b);
    int result = a.value.compareTo(b.value);
    if (a.id == b.id) {
        return 0;
    }
    return result == 0 ? Integer.compare(a.id, b.id) : result;
});
set.addAll(Arrays.asList(obja, objb, objc, objd));
System.out.println(set);

它打印出 [(1a), (2c)],从而删除了重复项。

但当我改变最后一个Integer.compare to Integer.compare(b.id, a.id)(即交换a和b的位置),它打印出[(2a), (1a), (2c)]。显然相同的 id 2 出现了两次。

如何修复比较器以始终根据 ids 删除重复项并根据值(升序)然后 id(降序)对有序集进行排序?


你问的是:
如何修复比较器以始终根据 ids 删除重复项并根据值(升序)然后 id(降序)对有序集进行排序?

您希望比较器

  1. 根据删除重复项Obj.id
  2. 对集合进行排序Obj.value and Obj.id

要求 1) 结果为

Function<Obj, Integer> byId = o -> o.id;
Set<Obj> setById = new TreeSet<>(Comparator.comparing(byId));

要求 2) 结果为

Function<Obj, String> byValue = o -> o.value;
Comparator<Obj> sortingComparator =  Comparator.comparing(byValue).thenComparing(Comparator.comparing(byId).reversed());
Set<Obj> setByValueAndId = new TreeSet<>(sortingComparator);

让我们来看看JavaDoc https://docs.oracle.com/javase/10/docs/api/java/util/TreeSet.html of TreeSet。它说:

请注意,集合 [...] 维护的顺序必须与equals如果是为了 正确实施Set界面。是这样的 因为Set接口是根据以下定义的equals手术, 但是一个TreeSet实例使用其执行所有元素比较compareTo(或比较)方法,因此两个元素被视为相等 通过这种方法,从集合的角度来看,它们是相等的。

该集合将根据比较器进行排序,但也会使用比较器比较其元素是否相等。

据我所知,没有办法定义Comparator满足这两个要求。自从一个TreeSet首先是aSet要求 1) 必须匹配。要实现要求 2),您可以创建第二个TreeSet:

Set<Obj> setByValueAndId = new TreeSet<>(sortingComparator);
setByValueAndId.addAll(setById);

或者,如果您不需要集合本身,但要按所需顺序处理元素,您可以使用Stream:

Consumer<Obj> consumer = <your consumer>;
setById.stream().sorted(sortingComparator).forEach(consumer);

BTW:
虽然可以对 a 的元素进行排序Stream根据给定的Comparator没有distinct方法采用Comparator根据它删除重复项。


EDIT:
您有两个不同的任务:1.重复删除,2.排序。一Comparator无法解决这两个任务。那么还有什么替代方案呢?

您可以覆盖equals and hashCode on Obj。然后一个HashSet or a Stream可用于删除重复项。
对于排序,您仍然需要Comparator(如上图所示)。实施Comparable仅用于排序将导致排序与根据的“与等于”不一致Comparable JavaDoc https://docs.oracle.com/javase/10/docs/api/java/lang/Comparable.html.

Since a Stream可以解决这两个任务,这将是我的选择。首先我们重写hashCode and equals通过以下方式识别重复项id:

public int hashCode() {
    return Integer.hashCode(id);
}

@Override
public boolean equals(Object obj) {
    if (this == obj)
        return true;
    if (obj == null)
        return false;
    if (getClass() != obj.getClass())
        return false;
    Obj other = (Obj) obj;
    if (id != other.id)
        return false;
    return true;
}

现在我们可以使用Stream:

// instantiating one additional Obj and reusing those from the question
Obj obj3a = new Obj(3, "a");

// reusing sortingComparator from the code above
Set<Obj> set = Stream.of(obja, objb, objc, objd, obj3a)
        .distinct()
        .sorted(sortingComparator)
        .collect(Collectors.toCollection(LinkedHashSet::new));

System.out.println(set); // [(3a), (1a), (2c)]

返回的LinkedHashSet具有以下语义Set但它也保留了顺序sortingComparator.


编辑(回答评论中的问题)

Q: 为什么它没有正确完成工作?
亲自看看吧。更改你的最后一行Comparator就像下面这样

int r = result == 0 ? Integer.compare(a.id, b.id) : result;
System.out.println(String.format("a: %s / b: %s / result: %s -> %s", a.id, b.id, result, r));
return r;

运行代码一次,然后切换操作数Integer.compare。切换导致不同的比较路径。区别在于当(2a) and (1a)进行比较。

在第一次运行中(2a)大于(1a)所以它与下一个条目进行比较(2c)。这会导致相等 - 找到重复项。

在第二轮比赛中(2a)小于(1a). Thus (2a)将作为下一个与上一个条目进行比较。但(1a)已经是最小的条目并且没有前一个条目。因此没有找到重复项(2a)并将其添加到集合中。

Q: 您说一个比较器无法完成两项任务,我的第一个比较器实际上正确地完成了两项任务。
是的 - 但仅限于给定的示例。添加Obj obj3a像我一样设置并运行你的代码。返回的排序集为:

[(1a), (3a), (2c)]

这违反了您对平等排序的要求values 下降id。现在它正在上升id。运行我的代码,它返回正确的顺序,如上所示。

Struggling with a Comparator a time ago I got the following comment: "... it’s a great exercise, demonstrating how tricky manual comparator implementations can be ..." (source https://stackoverflow.com/questions/51519499/java-8-streams-find-element-and-add-it-to-the-start-of-the-new-list/51520311#comment90014480_51520311)

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

TreeSet Comparator 在某些情况下无法删除重复项? 的相关文章

随机推荐