equals和hashCode:Objects.hash方法是否存在问题?

6
我正在使用Java 7,并且我有以下的类。我已经正确实现了equalshashCode,但问题是在下面的主方法中equals返回false,但hashCode为两个对象返回相同的哈希码。我能否请更多人来查看这个类,看看我是否有任何错误? 更新:我用自己的哈希函数替换了调用Objects.hash方法的行:chamorro.hashCode() + english.hashCode() + notes.hashCode()。它返回一个不同的哈希码,这正是当两个对象不同时hashCode应该做的事情。是Objects.hash方法坏了吗?
非常感谢您的帮助!
import org.apache.commons.lang3.StringEscapeUtils;

public class ChamorroEntry {

  private String chamorro, english, notes;

  public ChamorroEntry(String chamorro, String english, String notes) {
    this.chamorro = StringEscapeUtils.unescapeHtml4(chamorro.trim());
    this.english = StringEscapeUtils.unescapeHtml4(english.trim());
    this.notes = notes.trim();
  }

  @Override
  public boolean equals(Object object) {
    if (!(object instanceof ChamorroEntry)) {
      return false;
    }
    if (this == object) {
      return true;
    }
    ChamorroEntry entry = (ChamorroEntry) object;
    return chamorro.equals(entry.chamorro) && english.equals(entry.english)
        && notes.equals(entry.notes);
  }

  @Override
  public int hashCode() {
    return java.util.Objects.hash(chamorro, english, notes);
  }

  public static void main(String... args) {
    ChamorroEntry entry1 = new ChamorroEntry("Åguigan", "Second island south of Saipan. Åguihan.", "");
    ChamorroEntry entry2 = new ChamorroEntry("Åguihan", "Second island south of Saipan. Åguigan.", "");
    System.err.println(entry1.equals(entry2)); // returns false
    System.err.println(entry1.hashCode() + "\n" + entry2.hashCode()); // returns same hash code!
  }
}

2
两个对象具有相同的哈希码但仍不相等是可以的。所以问题不在于Objects.hash,对吧? - Ray Toal
@RayToal 这没问题,但很愚蠢。考虑到有 40 亿个可能的值,碰撞应该非常少,您不应该通过手动尝试几个值来遇到它们。Objects.hash 在设计上有点愚蠢,因为重复使用相同的乘数会导致这种低效率。 - maaartinus
4个回答

13

实际上,你碰巧触发了纯巧合。 :)

Objects.hash的实现方式是将每个给定对象的哈希码逐个相加,然后将结果乘以31,而String.hashCode则对其每个字符执行相同的操作。偶然的是,您使用的"英语"字符串中的差异与相同差异在"查莫罗"字符串结束位置之后恰好相差一个偏移量,因此一切都完美地抵消了。恭喜!

尝试使用其他字符串,您可能会发现它按预期工作。正如其他人已经指出的那样,严格来说,这种效果并不实际上是错误的,因为即使表示的对象不相等,哈希码也可能正确地冲突。如果有必要,可能值得尝试找到更高效的哈希,但我认为在现实情况下这应该是不必要的。


7

不要求不相等的对象必须具有不同的哈希码。预期相等的对象具有相等的哈希码,但哈希碰撞并非被禁止。return 1; 如果不是很有用,则可以作为hashCode的合法实现。

毕竟只有32位的可能哈希码,并且有无限数量的可能对象:) 哈希碰撞有时会发生。


4

HashCode是32位整型值,出现碰撞(两个对象具有相同的哈希码)的可能性始终存在,但很罕见/巧合。你提供的示例正是其中一个高度巧合的情况。以下是解释:

当您调用Objects.hash时,它内部使用以下逻辑调用Arrays.hashCode()

public static int hashCode(Object a[]) {
    if (a == null)
        return 0;
    int result = 1;
    for (Object element : a)
        result = 31 * result + (element == null ? 0 : element.hashCode());
    return result;
}

您的三个参数hashCode将得到以下结果:

   31 * (31 * (31 *1 +hashOfString1)+hashOfString2) + hashOfString3

对于您的第一个对象。个别字符串的哈希值为:

chamorro --> 1140493257 english --> 1698758127 notes --> 0

对于第二个对象:

chamorro --> 1140494218 english --> 1698728336 notes -->0

如果您注意到,两个对象的哈希码的前两个值不同。

但是当计算最终哈希码时:

  int hashCode1 = 31*(31*(31+1140493257) + 1698758127)+0;
  int hashCode2 = 31*(31*(31+1140494218) + 1698728336)+0;

巧合的是,它们产生了相同的哈希码1919283673,因为int存储在32位中。

通过使用下面的代码片段验证该理论:

  public static void main(String... args) {
    ChamorroEntry entry1 = new ChamorroEntry("Åguigan", 
                         "Second island south of Saipan. Åguihan.", "");
    ChamorroEntry entry2 = new ChamorroEntry("Åguihan", 
                         "Second island south of Saipan. Åguigan.", "");
    System.out.println(entry1.equals(entry2)); // returns false
    System.out.println("Åguigan".hashCode());
    System.out.println("Åguihan".hashCode());
    System.out.println("Second island south of Saipan. Åguihan.".hashCode());
    System.out.println("Second island south of Saipan. Åguigan.".hashCode());
    System.out.println("".hashCode());
    System.out.println("".hashCode());
    int hashCode1 = 31*(31*(31+1140493257) + 1698758127)+0;
    int hashCode2 = 31*(31*(31+1140494218) + 1698728336)+0;
    System.out.println(entry1.hashCode() + "\n" + entry2.hashCode()); 
    System.out.println(getHashCode(
                    new String[]{entry1.chamorro, entry1.english, entry1.notes}) 
                    + "\n" + getHashCode(
                    new String[]{entry2.chamorro, entry2.english, entry2.notes})); 
    System.out.println(hashCode1 + "\n" + hashCode2); // returns same hash code!
  }

    public static int getHashCode(Object a[]) {
        if (a == null)
            return 0;
        int result = 1;
        for (Object element : a)
            result = 31 * result + (element == null ? 0 : element.hashCode());
        return result;
    }

如果您使用不同的字符串参数,希望它将导致不同的hashCode。

2

两个不相等的对象没有必要有不同的哈希值,重要的是对于两个相等的对象具有相同的哈希值。

我可以像这样实现hashCode():

public int hashCode() {
    return 5;
}

它将保持正确(但效率低)。


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