在哈希表和可比表中使用的Java空对象

在哈希表和可比表中使用的Java空对象,java,equals,hashcode,equality,comparable,Java,Equals,Hashcode,Equality,Comparable,我有一个表示未知值的对象,或“Null对象” 与SQL中一样,此对象不应等于任何对象,包括另一个未知,因此(未知==未知)->false 但是,该对象在哈希表中使用,并且类型为Comparable,因此我创建了一个类,如下所示: public class Tag implements Comparable { final static UNKNOWN = new Tag("UNKNOWN"); String id; public Tag(String id) {

我有一个表示
未知值的对象,或“Null对象”

与SQL中一样,此对象不应等于任何对象,包括另一个
未知
,因此
(未知==未知)->false

但是,该对象在哈希表中使用,并且类型为Comparable,因此我创建了一个类,如下所示:

public class Tag implements Comparable {    
  final static UNKNOWN = new Tag("UNKNOWN");
  String id;

  public Tag(String id) { 
    this.id = id; 
  }

  public int hashCode(){
    return id.hashCode();
  }

  public String toString(){
    return id;
  }

  public boolean equals(Object o){    
    if (this == UNKNOWN || o == UNKNOWN || o == null || !(o instanceof Tag))
      return false;

    return this.id.equals(((Tag)o).id);
  }

  public int compareTo(Tag o){
    if (this == UNKNOWN)
      return -1;
    if (o == UNKNOWN || o == null)
      return 1;

    return this.id.compareTo(o.id);
  }
}
但是现在
compareTo()
似乎“不一致”

有没有更好的方法来实现
compareTo()

的文档中提到了这种情况:

强烈建议,但并非严格要求:

(x.compareTo(y)==0) == (x.equals(y))
一般来说,任何实现
Compariable
接口并违反此条件的类都应该清楚地指出这一事实。建议使用的语言是“注意:此类具有与equals不一致的自然顺序。”

因此,如果您希望对象具有可比性,但仍然不允许两个未知对象通过
equals
方法相等,则必须使您的
compareTo
与equals不一致

适当的实施办法是:

public int compareTo(Tag t) {
    return this.id.compareTo(t.id);
}
否则,您可以明确指出,
未知
值尤其是不可比较的值

public static boolean isUnknown(Tag t) {
    return t == UNKNOWN || (t != null && "UNKNOWN".equals(t.id));
}

public int compareTo(Tag t) {
    if (isUnknown(this) || isUnknown(t)) {
        throw new IllegalStateException("UNKNOWN is not Comparable");
    }
    return this.id.compareTo(t.id);
}

答案很简单:你不应该

你在这里有矛盾的要求。您的标记对象有一个隐式的顺序(这是Compariable表示的),或者您可以有这样的“特殊”值,这些值不等于任何东西,甚至不等于它们本身

正如另一个极好的答案和评论所指出的:是的,你可以以某种方式到达那里;例如,通过简单地同时允许a.compare(b)<0和b.compare(a)<0;或者抛出一个异常

但是我会非常小心。您正在违反一份已确立的合同。事实上,一些javadoc说:“打破合同是可以的”并不是关键——打破合同意味着所有从事这个项目的人都必须理解这个细节

从这里开始:如果a或b未知,您可以继续并在
compareTo()中抛出异常;通过这样做,您至少可以清楚地知道,例如,不应该尝试对
列表进行
排序()。但是,嘿,等等,你怎么会发现你的名单上有未知的东西呢?因为,你知道,UNKNOWN.equals(UNKNOWN)返回false;而
contains()
使用的是equals

本质上:虽然技术上可行,但无论你走到哪里,这种方法都会导致故障。意思:SQL支持这个概念并不意味着您应该在java代码中强制执行类似的内容。如前所述:这种想法非常“不合标准”;而且很容易让任何人看到它都感到惊讶。又名“意外行为”又名bug。

几秒钟的批判性思考: Java中已经有一个
null
,出于某种原因,您不能将其用作键

如果您尝试使用的密钥不等于其他任何密钥,包括 本身,您可以从不检索与该键关联的值


您的
compareTo()
方法现在不一致,这是正确的。它违反了此方法的几个要求。
compareTo()
方法必须提供域中值的总顺序。特别是,如评论中所述,
a.compareTo(b)<0
必须暗示
b.compareTo(a)>0
。此外,
a.compareTo(a)==0
对于每个值都必须为真

如果您的
compareTo()
方法不能满足这些要求,那么API的各个部分都将崩溃。例如,如果对包含
未知值的列表进行排序,则可能会出现可怕的“比较方法违反其一般约定!”异常

这与SQL中的
null
值不相等的要求有什么关系

对于SQL,答案是它在某种程度上扭曲了自己的规则。在你引用的维基百科文章中,有一个章节涵盖了类似的行为。虽然
null
值不被视为彼此相等,但它们也被视为彼此“不不同”,这允许
groupby
将它们分组在一起。(我在这里检测到一些规范性的黄鼠狼措辞。)对于排序,SQL要求
ORDER BY
子句首先有额外的
null
null LAST
,以便使用null进行排序

那么Java如何处理具有相似属性的IEEE 754
NaN
?应用于
NaN
的任何比较运算符的结果均为false。特别是,
NaN==NaN
为false。这似乎不可能对浮点值进行排序,也不可能将它们用作映射中的键。事实证明,Java有自己的一组特殊情况。如果您查看和的规范,它们有专门的案例,正好涵盖这些情况。具体来说,

Double.NaN == Double.NaN // false

Double.valueOf(Double.NaN).equals(Double.NaN) // true!
另外,指定了
Double.compareTo()
,以便它认为
NaN
等于它本身(它与equals一致),并且
NaN
被认为比其他所有
Double
值都大,包括
正数

还有一种实用方法,它使用相同的语义比较两个原语
double

这些特殊情况使Java排序、映射等能够很好地处理
Double
值,即使这违反了IEEE 754。(但请注意,基本
double
值确实符合IEEE 754。)

这应该如何应用于您的
标记
类及其
未知
值?我认为这里不需要遵循SQL的
null
规则。如果在中使用
标记
public boolean equals(Object obj) {
    if (this == obj) {
        return true;
    }

    return obj instanceof Tag && id.equals(((Tag)obj).id);
}
public int compareTo(Tag o) {
    if (this.equals(o)) {
        return 0;
    }

    if (this.equals(UNKNOWN)) {
        return -1;
    }

    if (o.equals(UNKNOWN)) {
        return 1;
    }

    return id.compareTo(o.id);
}