章
目
录
在Java中,我们可以使用对象作为HashMap的key,但在这样做时需要考虑重要的因素和准则。这的确是一个常见的面试问题,需要对HashMap的工作原理有很好的理解。以下是用户定义的类用作HashMap中的键时,需要考虑的几个重要因素:
1. 键必须遵守hashCode()和equals()之间的契约
为了设计一个良好的键,基本需求是“我们应该能够在不失败的情况下从map中检索值对象”,否则无论你构建多么复杂的数据结构,都将毫无用处。
为了决定我们是否已经创建了一个良好的键,我们必须了解“HashMap的工作原理”。“HashMap的工作原理”部分请自行阅读相关文章,但总结来说,它是基于哈希的原则工作的。
在HashMap中,键的hashCode()主要与equals()方法一起使用,用于将键放入map中,然后从map中获取它。因此,我们的唯一关注点是这两个方法。
- equals() – 验证两个对象的相等性,这在我们的案例中是键。要重写以提供比较两个键的逻辑。
- hashCode() – 在运行时返回键的唯一整数值。这个值用于查找map中的桶位置。
通常,如果重写equals()方法,则必须重写hashCode()方法,以保持hashCode()方法的一般合同,该合同规定相等的对象必须具有相等的哈希码。
2. 如果允许更改键的hashCode会发生什么?
如上所述,哈希码有助于计算存储键值对的map中的桶位置。不同的哈希码值可能指的是不同的桶位置。
如果不小心,在将键值对放入map后,更改了键对象的哈希码,那么几乎不可能从map中获取值对象,因为我们不知道在哪个桶中放入了键值对。旧的键值对是不可访问的,因此这是一种内存泄漏情况。
在运行时,JVM为每个对象计算哈希码并按需提供。当我们修改对象的状态时,JVM设置一个标志,表示对象已被修改,并且必须再次计算哈希码。所以,下一次调用对象的hashCode()方法时,JVM会重新计算该对象的哈希码。
3. 我们应该使HashMap的键不可变
对于上述基本推理,建议键对象是不可变的。不可变性确保我们每次都会得到相同的哈希码,用于键对象。因此,它实际上一次性解决了几乎所有问题。但是,再次强调,这种类必须遵守hashCode()和equals()方法的合同。
这也是为什么像String、Integer或其他包装类这样的不可变类是良好的键对象候选项的主要原因。这也回答了为什么字符串是Java中流行的HashMap键的问题。
但请记住,不可变性是建议而不是强制性的。如果你想将可变对象用作HashMap中的键,那么你必须确保键对象的状态更改不会更改对象的哈希码。这可以通过重写hashCode()方法来实现。但是,你必须确保你遵守equals()的合同。
4. 自定义键的HashMap示例
在下面的示例中,我创建了一个Account类,仅包含两个字段以简化。我已经重写了hashCode和equals方法,以便它只使用帐户号来验证Account对象的唯一性。帐户类的所有其他可能属性都可以在运行时更改。
public class Account
{
private int accountNumber;
private String holderName;
public Account(int accountNumber) {
this.accountNumber = accountNumber;
}
public String getHolderName() {
return holderName;
}
public void setHolderName(String holderName) {
this.holderName = holderName;
}
public int getAccountNumber() {
return accountNumber;
}
//Depends only on account number
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + accountNumber;
return result;
}
//仅比较 account numbers
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Account other = (Account) obj;
if (accountNumber != other.accountNumber)
return false;
return true;
}
}
这是否会导致不良行为?
不会。原因是Account类的实现遵守了这样的合同:“只要它们相等,相等的对象必须产生相同的哈希码,但不相等的对象不需要产生不同的哈希码。”
5. 示例
让我们测试一下上述分析中的Account类。
//创建具有可变键的HashMap
HashMap<Account, String> map = new HashMap<Account, String>();
//创建 key 1
Account a1 = new Account(1);
a1.setHolderName("A_ONE");
//创建 key 2
Account a2 = new Account(2);
a2.setHolderName("A_TWO");
//在map中存入可变键和值
map.put(a1, a1.getHolderName());
map.put(a2, a2.getHolderName());
//更改key状态,哈希码便重新计算
a1.setHolderName("Defaulter");
a2.setHolderName("Bankrupt");
//成功!我们能够获取value
System.out.println(map.get(a1)); //打印 A_ONE
System.out.println(map.get(a2)); //打印 A_TWO
//尝试使用新创建的具有相同account number的key
Account a3 = new Account(1);
a3.setHolderName("A_THREE");
//成功!我们仍然能够取回账号1的value
System.out.println(map.get(a3)); //打印 A_ONE
6. 结论
在本教程中,我们学习了如何设计一个类,可以用作Map实例中存储键值对的键。
作为最佳实践:
- 键类应该是不可变的。
- 在大多数情况下,默认的hashCode()和equals()方法已经足够好,但如果重写了一个方法,那么应该重写另一个方法,以确保它们之间遵守合同。