如何实现 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 id
或 long id
),这对于您的 class 的每个实例都是唯一的(您可以有一个 static
计数器变量并使用它来初始化构造函数中的 id
)。
那么你的compareTo
方法可以先比较名字,如果名字相等,再比较id
。
由于每个实例都有不同的 id
,compareTo
永远不会 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位整数(长)的范围足够大,可以指望这一点。
我有一个 class,它的相等性(根据 equals()
) 必须 由对象标识定义,即 this == other
。
我想实现 Comparable
来订购这些对象(比如 getName()
属性)。要与equals()
保持一致,compareTo()
一定不能return0
,即使两个对象同名。
有没有办法在 compareTo
的意义上比较对象身份?我可以比较 System.identityHashCode(o)
,但在哈希冲突的情况下仍然 return 0
。
您可以添加第二个 属性(比如 int id
或 long id
),这对于您的 class 的每个实例都是唯一的(您可以有一个 static
计数器变量并使用它来初始化构造函数中的 id
)。
那么你的compareTo
方法可以先比较名字,如果名字相等,再比较id
。
由于每个实例都有不同的 id
,compareTo
永远不会 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位整数(长)的范围足够大,可以指望这一点。