[英]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(降序)對有序集進行排序?
您希望比較器
Obj.id
刪除重復Obj.id
Obj.value
和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);
我們來看看TreeSet
的JavaDoc 。 它說:
請注意,如果要正確實現
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);
順便提一句:
雖然可以根據給定的Comparator
對Stream
的元素進行排序,但沒有使用Comparator
根據它刪除重復項的distinct
方法。
編輯:
您有兩個不同的任務:1. 重復刪除,2. 排序。 一個Comparator
器不能同時解決這兩項任務。 那么有哪些替代方案呢?
您可以覆蓋Obj
上的equals
和hashCode
。 然后可以使用HashSet
或Stream
來刪除重復項。
對於排序,您仍然需要一個Comparator
(如上所示)。 根據Comparable
JavaDoc ,僅為了排序而實現Comparable
會導致排序不“與等於一致”。
由於Stream
可以解決這兩個任務,因此我會選擇它。 首先,我們覆蓋hashCode
和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)
和(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.