如何实现 Comparable 使其与身份平等一致

How to implement Comparable so it is consistent with identity-equality

我有一个 class,它的相等性(根据 equals() 必须 由对象标识定义,即 this == other

我想实现 Comparable 来订购这些对象(比如 getName() 属性)。要与equals()保持一致,compareTo()一定不能return0,即使两个对象同名。

有没有办法在 compareTo 的意义上比较对象身份?我可以比较 System.identityHashCode(o),但在哈希冲突的情况下仍然 return 0

您可以添加第二个 属性(比如 int idlong id),这对于您的 class 的每个实例都是唯一的(您可以有一个 static 计数器变量并使用它来初始化构造函数中的 id)。

那么你的compareTo方法可以先比较名字,如果名字相等,再比较id

由于每个实例都有不同的 idcompareTo 永远不会 return 0.

我认为真正的答案是:那就不要实现 Comparable。实现此接口意味着您的对象具有自然顺序。 "equal" 的东西在你跟进那个想法时应该在同一个地方。

如果有的话,您应该使用自定义比较器……但即便如此也没有多大意义。如果定义 a < b ... 的东西不允许给你 a == b (根据你的 < 关系,当 a 和 b 是 "equal" 时),那么 比较的整个方法 对于您的用例已损坏。

换句话说:仅仅因为您可以将代码放入 class 中,"somehow" 会产生您想要的结果...并不能使它成为 这样做的想法。

假设有两个同名的对象,如果 equals() returns false 那么 compareTo() 不应该 return 0. 如果这是什么你想做的,那么以下可以帮助:

  • 覆盖 hashcode() 并确保它不仅仅依赖于 name
  • 执行compareTo()如下:
public void compareTo(MyObject object) {
    this.equals(object) ? this.hashcode() - object.hashcode() : this.getName().compareTo(object.getName());
}

你有独特的物体,但正如 Eran 所说,你可能需要额外的 counter/rehash 代码来处理任何碰撞。

private static Set<Pair<C, C> collisions = ...;

@Override
public boolean equals(C other) {
    return this == other;
}

@Override
public int compareTo(C other) {
    ...
    if (this == other) {
        return 0
    }
    if (super.equals(other)) {
        // Some stable order would be fine:
        // return either -1 or 1
        if (collisions.contains(new Pair(other, this)) {
            return 1;
        } else if (!collisions.contains(new Pair(this, other)) {
            collisions.add(new Par(this, other));
        }
        return 1;
    }
    ...
}

所以请按照 Eran 的回答,或者将要求 如此 放在问题中。

  • 人们可能认为不相同的 0 比较的开销可以忽略不计。
  • 如果在某个时间点不再创建实例,则可能会研究理想哈希函数。这意味着您拥有所有实例的集合。

根据定义,通过为每个对象分配一个 Universally unique identifier (UUID) (or a Globally unique identifier, (GUID)) as it's identity property, the UUID is comparable, and consistent with equals. Java already has a UUID class,并且一旦生成,您可以只使用字符串表示形式进行持久化。专用 属性 还将确保身份在 versions/threads/machines 中保持稳定。如果您有确保所有内容都获得唯一 ID 的方法,您也可以只使用递增 ID,但使用标准 UUID 实现将保护您免受集合合并和并行系统同时生成数据的问题。

如果您使用其他任何东西作为可比项,这意味着它在与 identity/value 不同的方式上具有可比性。因此,您需要定义此对象的可比性含义,并记录下来。例如,人们可以通过姓名、出生日期、身高或优先顺序的组合进行比较;最自然地按名称作为约定(以便于人类查找)与两个人是否是同一个人分开。您还必须接受 compareto 和 equals 是不相交的,因为它们基于不同的事物。

虽然我坚持我原来的答案,即您应该使用 UUID 属性 来进行稳定一致的比较/相等设置,但我想我会继续回答 "how far could you go if you were REALLY paranoid and wanted a guaranteed unique identity for comparable" 的问题.

基本上,简而言之,如果你不相信 UUID 的唯一性或身份的唯一性,只要使用尽可能多的 UUID 就可以证明上帝正在积极密谋反对你。 (请注意,虽然在技术上不能保证不会抛出异常,但在任何理智的宇宙中需要 2 个 UUID 应该是过大的杀伤力。)

import java.time.Instant;
import java.util.ArrayList;
import java.util.UUID;

public class Test implements Comparable<Test>{

    private final UUID antiCollisionProp = UUID.randomUUID();
    private final ArrayList<UUID> antiuniverseProp = new ArrayList<UUID>();

    private UUID getParanoiaLevelId(int i) {
        while(antiuniverseProp.size() < i) {
            antiuniverseProp.add(UUID.randomUUID());
        }

        return antiuniverseProp.get(i);
    }

    @Override
    public int compareTo(Test o) {
        if(this == o)
            return 0;

        int temp = System.identityHashCode(this) - System.identityHashCode(o);
        if(temp != 0)
            return temp;

        //If the universe hates you
        temp = this.antiCollisionProp.compareTo(o.antiCollisionProp);
        if(temp != 0)
            return temp;

        //If the universe is activly out to get you
        temp = System.identityHashCode(this.antiCollisionProp) - System.identityHashCode(o.antiCollisionProp);;
        if(temp != 0)
            return temp;

        for(int i = 0; i < Integer.MAX_VALUE; i++) {
            UUID id1 = this.getParanoiaLevelId(i);
            UUID id2 = o.getParanoiaLevelId(i);
            temp = id1.compareTo(id2);
            if(temp != 0)
                return temp;

            temp = System.identityHashCode(id1) - System.identityHashCode(id2);;
            if(temp != 0)
                return temp;
        }

        // If you reach this point, I have no idea what you did to deserve this
        throw new IllegalStateException("RAGNAROK HAS COME! THE MIDGARD SERPENT AWAKENS!");
    }

}

有时(尽管很少见)有必要实施基于身份的 compareTo 覆盖。就我而言,我正在实施 java.util.concurrent.Delayed.

由于 JDK 也实现了这个 class,我想我会分享 JDK 的解决方案,它使用原子递增的序列号。这是 ScheduledThreadPoolExecutor 的一个片段(为清楚起见略作修改):


    /**
     * Sequence number to break scheduling ties, and in turn to
     * guarantee FIFO order among tied entries.
     */
    private static final AtomicLong sequencer = new AtomicLong();

    private class ScheduledFutureTask<V>
            extends FutureTask<V> implements RunnableScheduledFuture<V> {

        /** Sequence number to break ties FIFO */
        private final long sequenceNumber = sequencer.getAndIncrement();
        
    }

如果 compareTo 中使用的其他字段已用完,则使用此 sequenceNumber 值来打破平局。 64位整数(长)的范围足够大,可以指望这一点。