为什么我在 Java HashMap 中得到重复键?

Why am I getting duplicate keys in Java HashMap?

我似乎在标准 Java HashMap 中得到重复键。通过 "duplicate",我的意思是键通过它们的 equals() 方法是相等的。这是有问题的代码:

import java.util.Map;
import java.util.HashMap;

public class User {
    private String userId;
    public User(String userId) { 
        this.userId = userId;
    }
    public boolean equals(User other) {
        return userId.equals(other.getUserId());
    }
    public int hashCode() {
        return userId.hashCode();
    }
    public String toString() {
        return userId;
    }

    public static void main(String[] args) {
        User arvo1 = new User("Arvo-Part");
        User arvo2 = new User("Arvo-Part");
        Map<User,Integer> map = new HashMap<User,Integer>();
        map.put(arvo1,1);
        map.put(arvo2,2);

        System.out.println("arvo1.equals(arvo2): " + arvo1.equals(arvo2));
        System.out.println("map: " + map.toString());
        System.out.println("arvo1 hash: " + arvo1.hashCode());
        System.out.println("arvo2 hash: " + arvo2.hashCode());
        System.out.println("map.get(arvo1): " + map.get(arvo1));
        System.out.println("map.get(arvo2): " + map.get(arvo2));
        System.out.println("map.get(arvo2): " + map.get(arvo2));
        System.out.println("map.get(arvo1): " + map.get(arvo1));
    }
}

这是结果输出:

arvo1.equals(arvo2): true
map: {Arvo-Part=1, Arvo-Part=2}
arvo1 hash: 164585782
arvo2 hash: 164585782
map.get(arvo1): 1
map.get(arvo2): 2
map.get(arvo2): 2
map.get(arvo1): 1

如您所见,两个 User 对象的 equals() 方法返回 true 并且它们的哈希码相同,但它们各自形成不同的 keymap 中。此外,map 继续区分最后四个 get() 调用中的两个 User 键。

这与documentation直接矛盾:

More formally, if this map contains a mapping from a key k to a value v such that (key==null ? k==null : key.equals(k)), then this method returns v; otherwise it returns null. (There can be at most one such mapping.)

这是一个错误吗?我在这里错过了什么吗?我是 运行 Java 版本 1.8.0_92,我是通过 Homebrew 安装的。

编辑:此问题已被标记为与此 other question 重复,但我将保留此问题原样,因为它表明与 equals() 看似不一致,而另一个问题假设错误在于 hashCode()。希望这个问题的存在将使这个问题更容易被搜索到。

您的 equals 方法没有覆盖 equals,并且 Map 中的类型在运行时被删除,因此实际调用的 equals 方法是 equals(Object)。你的 equals 应该看起来更像这样:

@Override
public boolean equals(Object other) {
    if (!(other instanceof User))
        return false;
    User u = (User)other;
    return userId.equals(u.userId);
}

问题出在你的equals()方法上。 Object.equals() 的签名是 equals(OBJECT),但在你的情况下它是 equals(USER),所以这是两种完全不同的方法,hashmap 正在调用带有 Object 参数的方法。您可以通过在 equals 上放置 @Override 注释来验证这一点 - 它会产生编译器错误。

equals 方法应该是:

  @Override
  public boolean equals(Object other) {
    if(other instanceof User){
        User user = (User) other;
        return userId.equals(user.userId);
    }

    return false;
}

作为最佳实践,您应该始终将 @Override 放在您覆盖的方法上 - 它可以为您省去很多麻烦。

好的,所以首先,代码无法编译。缺少此方法:

other.getUserId()

但除此之外,您还需要 @Override equals 方法,IDE 像 Eclipse 也可以帮助生成 equalshashCode 顺便说一句。

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

正如 Chrylis 所建议的,通过将 @Override 添加到 hashCodeequals 中,您将得到一个编译错误,因为 equals 方法的签名是 public boolean equals(Object other),所以你实际上并没有覆盖默认的(来自 Object class)的 equals 方法。这导致两个用户最终都在 hashMap 内的同一个桶中(hashCode 被覆盖并且两个用户具有相同的哈希码),但是当检查相等性时它们是不同的,因为使用了默认的 equals 方法这意味着比较内存地址。

equals 方法替换为以下内容以获得预期结果:

@Override
public boolean equals(Object other) {
    return getUserId().equals(((User)other).getUserId());
}

就像其他人回答的那样,您遇到了 equals 方法签名的问题。根据 Java equals best practice,你应该像下面这样实现 equals :

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;

    User user = (User) o;

    return userId.equals(user.userId);
  }

同样适用于 hashCode() 方法。见 Overriding equals() and hashCode() method in Java

The Second Problem

你现在没有重复了,但是你有一个新问题,你的 HashMap 只包含一个元素:

map: {Arvo-Part=2}

这是因为两个 User 对象都引用相同的字符串 (JVM String Interning),并且从 HashMap 的角度来看,您的两个对象是相同的,因为两个对象在哈希码和等于方法。因此,当您将第二个对象添加到 HashMap 时,您会覆盖第一个对象。 为避免此问题,请确保为每个用户使用唯一 ID

A simple demonstration on your users :