如果没有重写hashCode()方法,HashSet允许插入重复的项。

22
class temp {
int id;

public int getId() {
  return id;
}

temp(int id) {
  this.id = id;
}

public void setId(int id) {
  this.id = id;
}

@Override
public boolean equals(Object obj) {
  if (this == obj)
      return true;
  if (obj == null)
      return false;
  if (getClass() != obj.getClass())
      return false;
  temp other = (temp) obj;
  if (id != other.id)
      return false;
  return true;
}
}

public class testClass {

    public static void main(String[] args) {
      temp t1 = new temp(1);
      temp t2 = new temp(1);
      System.out.println(t1.equals(t2));
      Set<temp> tempList = new HashSet<temp>(2);
      tempList.add(t1);
      tempList.add(t2);
      System.out.println(tempList);
}

这个程序将两个元素都添加到了Set中。一开始我很惊讶,因为在向集合中添加方法时,会调用equals方法。

但是后来我重写了hashCode方法:

@Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + id;
        return result;
    }

然后它没有被添加进去。这很奇怪,因为 Set 和 add() 方法的 Javadoc 声明在添加元素时只检查 equals() 方法。

以下是 add() 的 javadoc:

/**
     * Adds the specified element to this set if it is not already present.
     * More formally, adds the specified element <tt>e</tt> to this set if
     * this set contains no element <tt>e2</tt> such that
     * <tt>(e==null&nbsp;?&nbsp;e2==null&nbsp;:&nbsp;e.equals(e2))</tt>.
     * If this set already contains the element, the call leaves the set
     * unchanged and returns <tt>false</tt>.
     *
     * @param e element to be added to this set
     * @return <tt>true</tt> if this set did not already contain the specified
     * element
     */
    public boolean add(E e) {
      return map.put(e, PRESENT)==null;
    }

后来我意识到HashSet是以HashMap的形式实现的,在map中,对象的hashCode被用作键。因此,如果您不覆盖hashCode,它将使用不同的键对待它们。

add()方法或HashSet的文档中应该有这个说明吗?


2
hashCode() 函数存在的整个原因是为了基于哈希的集合。如果您没有定义哈希函数,那么集合应该如何“知道”使用哪个哈希函数呢? - Peter Lawrey
2
只有在hashCode()相同时才会调用equals,如果你重写了equals,它不会调用默认的equals - Peter Lawrey
1
@PeterLawrey: 哦,原来是这样做的。谢谢你提供的信息,非常有帮助。 - Dhwaneet Bhatt
@DhwaneetBhatt 为什么在 equals(); 中使用 Object 类型参数,如果你仍然需要进行强制类型转换? - Asif Mushtaq
@UnKnown:我不明白你在问什么。 - Dhwaneet Bhatt
显示剩余2条评论
4个回答

21

这种情况已经有文档记载了。请参阅java.lang.Object的文档,其中在hashCode()一节中指出:

如果根据equals(Object)方法,两个对象相等,则调用这两个对象的hashCode()方法必须产生相同的整数结果。

此外,在Object.equals(Object)方法的文档中还发现以下内容:

请注意,通常需要覆盖hashCode()方法,以便在覆盖此方法时保持hashCode()方法的一般约定,即相等的对象必须具有相等的哈希码

换句话说,如果使用您的类时,instanceA.equals(instanceB) == trueinstanceA.hashCode() != istanceB.hashCode(),则实际上违反了Object类的规范。


1
我经常读到equals()和hashCode()都应该被重写,但我从未知道不这样做会产生不可预测的结果。而且由于Set和add()方法的文档明确说明覆盖equals方法是必要的,但没有提到hashCode()。契约已经解释了所有这些。 - Dhwaneet Bhatt
@DhwaneetBhatt,我很高兴能够提供帮助。确实,在Java的集合中存储对象时,实现这两种方法都很重要。我并不是说在没有明显或立即需要的情况下就可以忽略它。然而,一个适当的(根据契约)实现可能相当复杂,需要仔细考虑。 - Kallja
1
请注意,Java Set是一个接口。add(Object)方法在接口中定义,并且不知道实现。集合的不同实现需要从包含的对象中获取不同的信息。HashSet查询对象哈希码,而TreeSet需要一种比较对象的方式,不关心哈希码。 - Emperor Orionii

14

请参阅equals()文档:

请注意,每当重写此方法时,通常需要重写hashCode方法,以便维护hashCode方法的一般约定,该约定规定相等的对象必须具有相等的哈希码

事实上,equals()hashCode()是密切相关的。在处理其中一个方法时应始终考虑两个方法,以避免这些一致性问题。


杰克质疑为什么equals()方法的参数是Object类型?即使进行了强制转换,为什么不直接使用temp类类型作为参数? - Asif Mushtaq

8

如果你覆盖了equals()方法,你必须同时覆盖hashCode()方法。

对于equals()和hashCode()方法的行为,有一些限制规定在Object文档中。特别是,equals()方法必须具备以下特点:

  • 对于两个引用a和b,当且仅当b.equals(a)时,a.equals(b)
  • 对于所有非空引用a,a.equals(a)
  • 传递性:如果a.equals(b)并且b.equals(c),则a.equals(c)
  • 与hashCode()方法一致性:两个相等的对象必须具有相同的hashCode()值

更多详情请参见此处


1
多有趣啊,世界上的一切都受到数学的启发。 ;) - Dhwaneet Bhatt

1

他们(javadoc的开发人员)可能预先假定了(在HashSetadd()方法的文档中所说的),

(e==null ? e2==null : e.equals(e2))

hashCode() 对于它们两者来说本质上是相等的。


1
同意,尽管这个假设是有效的,因为使用Set的人应该熟悉相关的方法,比如Object.equals(Object),根据相应方法的文档,它与Object.hashCode()方法强相关。 - Kallja

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接