簡體   English   中英

當沒有區別字段時,比較器適用於TreeSet

[英]Comparator suitable for TreeSet when there is no distinguishing field

假設我有一個沒有實現Comparable接口的類

class Dummy {
}

以及該類的一個實例的集合以及該類外部的一些函數,它們允許部分地比較這些實例(下面將使用一個映射):

Collection<Dummy> col = new ArrayList<>();
Map<Dummy, Integer> map = new HashMap<>();
for (int i = 0; i < 12; i++) {
    Dummy d = new Dummy();
    col.add(d);
    map.put(d, i % 4);
}

現在我想使用帶有自定義比較器的TreeSet類對此集合進行排序:

TreeSet<Dummy> sorted = new TreeSet<>(new Comparator<Dummy>() {
    @Override
    public int compare(Dummy o1, Dummy o2) {
        return map.get(o1) - map.get(o2);
    }
});
sorted.addAll(col);

結果顯然不令人滿意(包含的元素少於初始集合)。 這是因為這樣的比較器equals不一致 ,即有時對於不相等的元素返回0 我的下一次嘗試是將compare器的compare方法更改為

@Override
public int compare(Dummy o1, Dummy o2) {
    int d = map.get(o1) - map.get(o2);
    if (d != 0)
        return d;
    if (o1.equals(o2))
        return 0;
    return 1; // is this acceptable?
}

它似乎為這個簡單的演示示例提供了期望的結果,但我仍然有疑問:總是為不等(但無法通過地圖)對象返回1是正確的嗎? 這樣的關系仍然違反Comparator.compare()方法的一般聯系,因為sgn(compare(x, y)) == -sgn(compare(y, x))通常是錯誤的。 我是否真的需要為TreeSet實現正確的總排序才能正常工作或上述是否足夠? 如果實例沒有可比較的字段,該怎么做?

對於更真實的例子,想象一下,你有一個泛型類的類型參數T ,而不是Dummy T可能有一些字段並通過它們實現equals()方法,但您不知道這些字段,但需要根據某些外部函數對此類的實例進行排序。 這是否可以在TreeSet的幫助下實現?

編輯

使用System.identityHashCode()是一個好主意,但有(不是很小) 碰撞的機會。

除了這種碰撞的可能性之外,還有一個陷阱 假設你有3個對象: abc這樣map.get(a) = map.get(b) = map.get(c) (這里=不是賦值但數學相等), identityHashCode(a) < identityHashCode(b) < identityHashCode(c)a.equals(c)為真,但a.equals(b) (因此c.equals(b) )為假。 按照以下順序將這3個元素添加到TreeSet之后: a, b, c您可以進入將所有這些元素添加到集合中的情況,這與Set接口的規定行為相矛盾 - 它不應包含相等的元素。 怎么處理?

另外,如果熟悉TreeSet機制的人向我解釋,在TreeSet javadoc中,短語“集合的行為即使其排序與equals不一致 也很明確”中的術語 定義良好”會是多么意思。

除非你有絕對大量的Dummy對象並且真的運氣不好,否則你可以使用System.identityHashCode()來打破關系:

Comparator.<Dummy>comparingInt(d -> map.get(d))
          .thenComparingInt(System::identityHashCode)

您的比較器是不可接受的,因為它違反了合同:如果它們不相等並且在地圖中沒有共享相同的值,則同時具有d1> d2和d2> d1。

這個答案僅涵蓋問題中的第一個例子。 問題的其余部分和各種編輯,我認為更好地回答作為單獨的,有針對性的問題的一部分。

第一個示例設置12個Dummy實例,創建一個映射,將每個實例映射到[0,3]范圍內的Integer ,然后將12個Dummy實例添加到TreeSet TreeSet提供了一個使用Dummy-to-Integer映射的比較器。 結果是TreeSet只包含四個Dummy實例。 該示例以以下語句結束:

結果顯然不令人滿意(包含的元素少於初始集合)。 這是因為這樣的比較器與equals不一致,即有時對於不相等的元素返回0。

這最后一句不正確。 結果包含的元素少於插入的元素,因為比較器認為許多實例是重復的,因此它們不會插入到集合中。 equals方法根本不進入討論。 因此,“與平等一致”的概念與本討論無關。 TreeSet從不調用equals 比較器是唯一決定TreeSet成員資格的東西。

這似乎是一個令人不滿意的結果,但只是因為我們“知道”有12個不同的Dummy實例。 但是, TreeSet並不“知道”它們是不同的。 它只知道如何使用比較器比較Dummy實例。 當它這樣做時,它發現有幾個是重復的。 也就是說,比較器有時返回0,即使它被我們認為是不同的Dummy實例調用。 這就是為什么只有四個Dummy實例最終出現在TreeSet

我不完全確定所期望的結果是什么,但似乎結果TreeSet應該包含由Dummy-to-Integer映射中的值排序的所有12個實例。 我的建議是使用Guava的Ordering.arbitrary() ,它提供了一個比較器來區分不同但不相同的元素,但這樣做的方式滿足了比較器的一般契約。 如果您像這樣創建TreeSet

SortedSet<Dummy> sorted = new TreeSet<>(Comparator.<Dummy>comparingInt(map::get)
                                                  .thenComparing(Ordering.arbitrary()));

結果將是TreeSet包含所有12個Dummy實例,按地圖中的Integer值排序,以及Dummy實例映射到任意排序的相同值。

在評論中,您聲明Ordering.arbitrary文檔“毫不含糊地警告不要在SortedSet中使用它”。 那不太對勁; 那個醫生說,

因為排序是基於身份的,所以它與Comparator定義的“與Object.equals(Object)”不一致。 從中構建SortedSet或SortedMap時要小心,因為生成的集合將不會完全按照規范運行。

短語“不完全按照規范行事”實際上意味着它會像Comparator的類doc中所描述的那樣“奇怪地”行為:

當且僅當c.compare(e1, e2)==0具有與每個e1的e1.equals(e2)相同的布爾值時,比較器c對一組元素S施加的排序被稱為與等於一致。和S中的e2

當使用能夠強加與equals不一致的排序的比較器來排序有序集(或有序映射)時,應該謹慎行事。 假設帶有顯式比較器c的有序集(或有序映射)與從集合S中繪制的元素(或鍵)一起使用。如果由S對S施加的排序與equals不一致,則排序集(或有序映射)將表現得“奇怪”。 特別是有序集(或有序映射)將違反集合(或映射)的一般契約,其以equals方式定義。

例如,假設有一個元素a和b添加兩個元素a (a.equals(b) && c.compare(a, b) != 0)到帶有比較器c的空TreeSet。 第二個add操作將返回true(並且樹集的大小將增加)因為a和b在樹集的透視圖中不等效,即使這與Set.add方法的規范相反。

您似乎表明這種“奇怪”行為是不可接受的,因為equals Dummy元素不應出現在TreeSet Dummy類不會覆蓋equals ,所以看起來這里還有一個額外的要求。

在稍后的問題編輯中添加了一些其他問題,但正如我上面提到的,我認為這些問題可以作為單獨的問題更好地處理。

更新2018-12-22

在重新閱讀編輯和評論的問題后,我想我終於找到了你想要的東西。 您希望在任何對象上使用比較器,該對象基於某些int值函數提供主要排序,這可能導致不等對象的重復值(由對象的equals方法確定)。 因此,需要二次排序,它提供所有不等對象的總排序,但對於equals對象,它返回零。 這意味着比較器應該與equals一致。

Guava的Ordering.arbitrary接近於它提供了對任何對象的任意總排序,但它只對相同的對象(即== )返回零,但對於equals對象則不返回零。 因此它與equals不一致。

那么,聽起來,你想要一個比較器,它可以在不等對象上提供任意排序。 這是一個創建一個的函數:

static Comparator<Object> arbitraryUnequal() {
    Map<Object, Integer> map = new HashMap<>();
    return (o1, o2) -> Integer.compare(map.computeIfAbsent(o1, x -> map.size()),
                                       map.computeIfAbsent(o2, x -> map.size()));
}

從本質上講,這會為每個新看到的不等對象分配一個序列號,並將這些數字保存在比較器所持有的地圖中。 它使用地圖的大小作為計數器。 由於永遠不會從此映射中刪除對象,因此大小和序列號總是會增加。

(如果您打算同時使用此比較器,例如,並行排序,則應使用ConcurrentHashMap替換HashMap ,並且應修改大小技巧以使用在添加新條目時遞增的AtomicInteger 。)

請注意,此比較器中的映射為它所見過的每個不等對象構建條目。 如果將其附加到TreeSet ,則對象將在比較器的映射中保留,即使它們已從TreeSet刪除。 這是必要的,這樣如果添加或刪除對象,它們將隨着時間的推移保持一致的排序。 Guava的Ordering.arbitrary使用弱引用來允許在不再使用對象時收集對象。 我們不能這樣做,因為我們需要保留不相同但相等的對象的順序。

你會這樣使用它:

SortedSet<Dummy> sorted = new TreeSet<>(Comparator.<Dummy>comparingInt(map::get)
                                                  .thenComparing(arbitraryUnequal()));

您還詢問了以下內容中“明確定義”的含義:

即使集合的順序與equals不一致,集合的行為也是明確定義的

假設您使用與equals不一致的比較器來使用TreeSet ,例如使用上面顯示的G​​uava的Ordering.arbitrary的比較器。 TreeSet仍將按預期工作,與自身一致。 也就是說,它將以總排序維護對象,它不包含比較器返回零的任何兩個對象,並且它的所有方法都將按指定的方式工作。 但是,有可能存在一個contains返回true的對象(因為它是使用比較器計算的),但是如果使用實際在集合中的對象調用,則equals為false。

例如, BigDecimalComparable但它的比較方法與equals不一致:

> BigDecimal z = new BigDecimal("0.0")
> BigDecimal zz = new BigDecimal("0.00")
> z.compareTo(zz)
0
> z.equals(zz)
false
> TreeSet<BigDecimal> ts = new TreeSet<>()
> ts.add(z)
> HashSet<BigDecimal> hs = new HashSet<>(ts)
> hs.equals(ts)
true
> ts.contains(zz)
true
> hs.contains(zz)
false

這就是當規范表明事情可能“奇怪”時,規范意味着什么。 我們有兩套是平等的。 然而,它們報告了contains相同對象的不同結果,並且TreeSet報告它包含一個對象,即使該對象與集合中的對象不相等。

這是我最終得到的比較器。 它既可靠又節省內存。

public static <T> Comparator<T> uniqualizer() {
    return new Comparator<T>() {
        private final Map<T, Integer> extraId = new HashMap<>();
        private int id;

        @Override
        public int compare(T o1, T o2) {
            int d = Integer.compare(o1.hashCode(), o2.hashCode());
            if (d != 0)
                return d;
            if (o1.equals(o2))
                return 0;
            d = extraId.computeIfAbsent(o1, key -> id++)
              - extraId.computeIfAbsent(o2, key -> id++);
            assert id > 0 : "ID overflow";
            assert d != 0 : "Concurrent modification";
            return d;
        }
    };
}

它在給定類T所有對象上創建總排序,從而允許通過附加到它來區分不能通過給定比較器區分的對象,如下所示:

Comparator<T> partial = ...
Comparator<T> total = partial.thenComparing(uniqualizer());

在問題中給出的例子中, TDummy

partial = Comparator.<Dummy>comparingInt(map::get);

請注意,在調用uniqualizer()時不需要指定類型T ,編譯器會通過類型推斷自動確定它。 您只需確保T中的hashCode()equals()一致,如hashCode()常規協定中所述。 然后uniqualizer()將為您提供與equals() )一致的比較器( total ),您可以在任何需要比較T類對象的代碼中使用它,例如在創建TreeSet

TreeSet<T> sorted = new TreeSet<>(total);

或排序列表:

List<T> list = ...
Collections.sort(list, total);

暫無
暫無

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

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