Java equals() 和 hashCode()
Java equals() 和 hashCode() 方法存在于 Object 类中。因此每个 Java 类都获得 equals() 和 hashCode() 的默认实现。在本文中,我们将详细研究 Java equals() 和 hashCode() 方法。
Java 的 equals()
Object 类定义 equals() 方法如下:
public boolean equals(Object obj) {
return (this == obj);
}
根据 equals() 方法的 java 文档,任何实现都应遵循以下原则。
- 对于任何对象 x,
x.equals(x)
都应该返回true
。 - 对于任何两个对象 x 和 y,当且仅当返回时才
x.equals(y)
应返回。true
y.equals(x)
true
- 对于多个对象 x、y 和 z,如果
x.equals(y)
返回true
并且y.equals(z)
返回true
,则x.equals(z)
应该返回true
。 - 多次调用
x.equals(y)
应该返回相同的结果,除非方法实现中所使用的任何对象属性被修改equals()
。 true
仅当两个引用都指向同一个对象时,对象类的 equals() 方法实现才会返回。
Java hashCode()
Java Object hashCode() 是一个本机方法,返回对象的整数哈希码值。 hashCode() 方法的一般约定是:
- 多次调用 hashCode() 应该返回相同的整数值,除非修改了在 equals() 方法中使用的对象属性。
- 对象哈希码值可能会在同一个应用程序的多次执行中发生变化。
- 如果两个对象根据equals()方法相等,则它们的哈希码必须相同。
- 如果两个对象根据 equals() 方法不相等,则它们的哈希码不必不同。它们的哈希码值可以相等,也可以不相等。
equals() 和 hashCode() 方法的重要性
Java hashCode() 和 equals() 方法用于 Java 中基于哈希表的实现,用于存储和检索数据。我在Java 中 HashMap 的工作原理中已经详细解释了这一点。equals () 和 hashCode() 的实现应遵循这些规则。
- 如果
o1.equals(o2)
,那么o1.hashCode() == o2.hashCode()
应该永远是true
。 - 如果
o1.hashCode() == o2.hashCode
是真的,那并不意味着o1.equals(o2)
就会是true
。
何时重写 equals() 和 hashCode() 方法?
当我们重写 equals() 方法时,几乎也必须重写 hashCode() 方法,这样我们的实现才不会违反它们的契约。请注意,如果违反了 equals() 和 hashCode() 契约,您的程序将不会抛出任何异常,如果您不打算将类用作哈希表键,则不会产生任何问题。如果您打算将类用作哈希表键,则必须重写 equals() 和 hashCode() 方法。让我们看看当我们依赖 equals() 和 hashCode() 方法的默认实现并使用自定义类作为 HashMap 键时会发生什么。
package com.journaldev.java;
public class DataKey {
private String name;
private int id;
// getter and setter methods
@Override
public String toString() {
return "DataKey [name=" + name + ", id=" + id + "]";
}
}
package com.journaldev.java;
import java.util.HashMap;
import java.util.Map;
public class HashingTest {
public static void main(String[] args) {
Map<DataKey, Integer> hm = getAllData();
DataKey dk = new DataKey();
dk.setId(1);
dk.setName("Pankaj");
System.out.println(dk.hashCode());
Integer value = hm.get(dk);
System.out.println(value);
}
private static Map<DataKey, Integer> getAllData() {
Map<DataKey, Integer> hm = new HashMap<>();
DataKey dk = new DataKey();
dk.setId(1);
dk.setName("Pankaj");
System.out.println(dk.hashCode());
hm.put(dk, 10);
return hm;
}
}
当我们运行上述程序时,它将打印null
。这是因为 Object hashCode() 方法用于查找存储桶以查找键。由于我们无权访问 HashMap 键,并且我们正在再次创建键来检索数据,您会注意到两个对象的哈希码值不同,因此找不到值。
实现 equals() 和 hashCode() 方法
我们可以定义自己的 equals() 和 hashCode() 方法实现,但如果我们不仔细实现它们,运行时可能会出现奇怪的问题。幸运的是,现在大多数 IDE 都提供了自动实现它们的方法,如果需要,我们可以根据需要更改它们。我们可以使用 Eclipse 自动生成 equals() 和 hashCode() 方法。以下是自动生成的 equals() 和 hashCode() 方法实现。
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + id;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
DataKey other = (DataKey) obj;
if (id != other.id)
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
请注意,equals() 和 hashCode() 方法都使用相同的字段进行计算,因此它们的约定仍然有效。如果您再次运行测试程序,我们将从 map 中获取对象,程序将打印 10。我们还可以使用Project Lombok自动生成 equals 和 hashCode 方法实现。
什么是哈希碰撞
简单来说,Java 哈希表实现使用以下逻辑进行获取和放置操作。
- 首先使用“键”哈希码识别要使用的“存储桶”。
- 如果存储桶中没有具有相同哈希码的对象,则添加该对象以进行放入操作并返回 null 以进行获取操作。
- 如果存储桶中还有其他对象具有相同的哈希码,则“key”equals 方法就会发挥作用。
- 如果 equals() 返回 true 并且它是一个 put 操作,那么对象值将被覆盖。
- 如果 equals() 返回 false 并且这是一个 put 操作,那么新条目将添加到存储桶中。
- 如果 equals() 返回 true 并且它是一个 get 操作,则返回对象值。
- 如果 equals() 返回 false 并且这是一个 get 操作,则返回 null。
下图显示了 HashMap 的 bucket 项以及它们的 equals() 和 hashCode() 之间的关系。两个键具有相同哈希码的现象称为哈希冲突。如果 hashCode() 方法未正确实现,则会出现大量哈希冲突,并且映射条目将无法正确分布,从而导致获取和放置操作缓慢。这就是在生成哈希码时使用素数的原因,以便映射条目可以正确分布在所有 bucket 中。
如果我们不同时实现 hashCode() 和 equals() 会怎么样?
我们已经看到,如果没有实现 hashCode(),我们将无法检索值,因为 HashMap 使用哈希码来查找要查找条目的存储桶。如果我们只使用 hashCode() 而不实现 equals(),那么也无法检索值,因为 equals() 方法将返回 false。
实现 equals() 和 hashCode() 方法的最佳实践
- 在 equals() 和 hashCode() 方法实现中使用相同的属性,以便在更新任何属性时不会违反它们的契约。
- 最好使用不可变对象作为哈希表键,这样我们就可以缓存哈希码,而不是在每次调用时计算它。这就是为什么 String 是哈希表键的良好候选者,因为它是不可变的并且缓存哈希码值。
- 实现 hashCode() 方法,以便发生最少数量的哈希冲突并且条目均匀分布在所有存储桶中。
您可以从我们的GitHub 存储库下载完整的代码。