为什么我在 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
并且它们的哈希码相同,但它们各自形成不同的 key
在 map
中。此外,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 也可以帮助生成 equals
和 hashCode
顺便说一句。
@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
添加到 hashCode
和 equals
中,您将得到一个编译错误,因为 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 :
我似乎在标准 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
并且它们的哈希码相同,但它们各自形成不同的 key
在 map
中。此外,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 也可以帮助生成 equals
和 hashCode
顺便说一句。
@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
添加到 hashCode
和 equals
中,您将得到一个编译错误,因为 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 :