TreeSet比较器在某些情况下无法删除重复项?

时间:2018-11-19 08:53:58

标签: java sorting comparator treeset

我的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更改为Integer.compare(b.id, a.id)时(即切换a和b的位置),它会打印出[(2a),(1a),(2c)]。显然,相同的ID 2出现了两次。

如何固定比较器,使其始终根据ID删除重复项,并根据值(升序)然后ID(降序)对有序集进行排序?

1 个答案:

答案 0 :(得分:4)

您在问:
您如何修复比较器,使其始终根据id删除重复项,并根据值(升序)然后id(降序)对有序集合进行排序?

您希望比较器

  1. 根据Obj.id删除重复项
  2. Obj.alueObj.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);

我们来看看TreeSet的{​​{3}}。它说:

  

请注意,集合[...]维护的顺序必须与equals一致   正确实现Set接口。就是这样   因为Set接口是根据equals操作定义的,   但是TreeSet实例使用其实例执行所有元素比较   compareTo(或比较)方法,因此两个元素被视为相等   从集合的角度来看,这种方法是相等的。

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

据我所知,无法定义同时满足这两个要求的Comparator。由于TreeSet首先是Set要求1)必须匹配。要达到要求2),您可以创建第二个TreeSet

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

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

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

顺便说一句:
尽管可以根据给定的StreamComparator的元素进行排序,但没有distinct的方法采用Comparator来删除重复项。


编辑:
您有两个不同的任务:1.重复删除,2.排序。一个Comparator不能解决两项任务。那有什么替代方法呢?

您可以覆盖equals上的hashCodeObj。然后可以使用HashSetStream删除重复项。
对于排序,您仍然需要Comparator(如上所示)。根据{{​​1}} JavaDoc,仅将Comparable用于排序将导致排序不等于“等于”。

由于Comparable可以解决这两个任务,所以这是我的选择。首先,我们覆盖StreamhashCode以通过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的最后一行

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)

在第一次运行中,(1a)大于(2a),因此将其与下一个条目(1a)进行比较。这导致相等-找到重复项。

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

Q:您说一个比较器无法完成两项任务,而我的第一个比较器实际上正确地完成了两项任务。
是的-但仅适用于给定的示例。像我一样将(2a)添加到集合中并运行您的代码。返回的排序集是:

Obj obj3a

这违反了您对以[(1a), (3a), (2c)] 降序的相等value进行排序的要求。现在它以id递增。运行我的代码,它返回正确的顺序,如上所示。

前段时间在id上挣扎,我得到以下评论:“ ...这是一个很棒的练习,展示了手动比较器实现有多么棘手……”(JavaDoc