簡體   English   中英

在某些情況下,TreeSet Comparator 無法刪除重復項?

[英]TreeSet Comparator failed to remove duplicates in some cases?

我的 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(降序)對有序集進行排序?

你問的是:
您如何修復比較器以始終根據 id 刪除重復項並根據值(升序)然后 id(降序)對有序集進行排序?

您希望比較器

  1. 根據Obj.id刪除重復Obj.id
  2. Obj.valueObj.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);

我們來看看TreeSetJavaDoc 它說:

請注意,如果要正確實現Set接口,則 set [...] 維護的 ordering 必須與equals一致。 這是因為Set接口是根據equals操作定義的,但是TreeSet實例使用它的compareTo (或 compare)方法執行所有元素比較,因此從該方法的角度來看,兩個元素被認為相等設置,相等。

該集合將根據比較器進行排序,但也使用比較器比較其元素是否相等。

據我所知,沒有辦法定義一個滿足這兩個要求的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);

順便提一句:
雖然可以根據給定的ComparatorStream的元素進行排序,但沒有使用Comparator根據它刪除重復項的distinct方法。


編輯:
您有兩個不同的任務:1. 重復刪除,2. 排序。 一個Comparator器不能同時解決這兩項任務。 那么有哪些替代方案呢?

您可以覆蓋Obj上的equalshashCode 然后可以使用HashSetStream來刪除重復項。
對於排序,您仍然需要一個Comparator (如上所示)。 根據Comparable JavaDoc ,僅為了排序而實現Comparable會導致排序不“與等於一致”。

由於Stream可以解決這兩個任務,因此我會選擇它。 首先,我們覆蓋hashCodeequals以通過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)(1a)

在第一次運行中(2a)大於(1a)所以它與下一個條目(2c) 這導致相等 - 發現重復。

在第二輪中(2a)小於(1a) 因此(2a)將作為下一個與前一個條目進行比較。 但是(1a)已經是最小的條目並且沒有之前的條目。 因此沒有發現(2a)重復項並將其添加到集合中。

問:你說一個比較器不能完成兩個任務,我的第一個比較器實際上正確地完成了兩個任務。
是的 - 但僅適用於給定的示例。 像我一樣將Obj obj3a添加到集合中並運行您的代碼。 返回的排序集是:

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

這違反您的要求進行排序同等value由s下行id 現在它按id升序。 運行我的代碼,它返回正確的順序,如上所示。

前一段時間與Comparator苦苦掙扎時,我得到了以下評論:“......這是一個很好的練習,展示了手動比較器實現是多么棘手......”( 來源

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM