我们为什么要覆盖 equals() 方法
在 Java 中,我们不能重载 ==、+=、-+ 等运算符的行为方式。他们以某种方式行事。因此,让我们在这里关注运算符 ==。
运算符 == 的工作原理。
它检查我们比较的 2 个引用是否指向内存中的同一个实例。只有当这 2 个引用代表内存中的同一个实例时,运算符 == 才会解析为 true。
所以现在让我们考虑下面的例子
public class Person {
private Integer age;
private String name;
..getters, setters, constructors
}
假设在您的程序中,您在不同的地方构建了 2 个 Person 对象,并且您希望对它们进行比较。
Person person1 = new Person("Mike", 34);
Person person2 = new Person("Mike", 34);
System.out.println ( person1 == person2 ); --> will print false!
从业务角度来看,这 2 个对象看起来一样吧?对于 JVM,它们是不一样的。由于它们都是使用new 关键字创建的,因此这些实例位于内存中的不同段中。因此运算符 == 将返回 false
但是如果我们不能覆盖 == 运算符,我们怎么能告诉 JVM 我们希望这两个对象被视为相同。 .equals() 方法在起作用。
您可以覆盖equals() 以检查某些对象是否具有相同的特定字段值以被视为相等。
您可以选择要比较的字段。如果我们说 2 个 Person 对象当且仅当它们具有相同的年龄和相同的名称时才会是相同的,那么 IDE 将创建类似以下的内容来自动生成 equals()
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age &&
name.equals(person.name);
}
让我们回到之前的例子
Person person1 = new Person("Mike", 34);
Person person2 = new Person("Mike", 34);
System.out.println ( person1 == person2 ); --> will print false!
System.out.println ( person1.equals(person2) ); --> will print true!
所以我们不能重载 == 运算符来按照我们想要的方式比较对象,但是 Java 给了我们另一种方式,equals() 方法,我们可以根据需要重写它。
请记住但是,如果我们不在我们的类中提供自定义版本的 .equals()(也称为覆盖),那么来自 Object 类的预定义 .equals() 和 == 运算符将行为完全相同。
从Object继承的默认equals()方法将检查两个比较实例在内存中是否相同!
我们为什么要覆盖 hashCode() 方法
Java 中的一些数据结构(如 HashSet、HashMap)基于应用于这些元素的哈希函数来存储它们的元素。哈希函数是hashCode()
如果我们可以选择覆盖.equals() 方法,那么我们也必须选择覆盖hashCode() 方法。这是有原因的。
从 Object 继承的 hashCode() 的默认实现认为内存中的所有对象都是唯一的!
让我们回到那些哈希数据结构。这些数据结构有一个规则。
HashSet不能包含重复值,HashMap不能包含重复键
HashSet 在后台使用 HashMap 实现,其中 HashSet 的每个值都作为键存储在 HashMap 中。
所以我们必须了解 HashMap 的工作原理。
简单来说,HashMap 是一个包含一些桶的原生数组。每个桶都有一个linkedList。在该linkedList 中存储了我们的密钥。 HashMap 通过应用hashCode() 方法为每个键定位正确的linkedList,然后遍历该linkedList 的所有元素,并对每个元素应用equals() 方法以检查该元素是否已包含在那里。不允许有重复的键。
当我们在 HashMap 中放入一些东西时,键存储在其中一个链接列表中。该键将存储在哪个linkedList 中,由该键上的hashCode() 方法的结果显示。因此,如果key1.hashCode() 的结果为 4,则该 key1 将存储在数组的第 4 个存储桶中,即存在的linkedList 中。
默认情况下,hashCode() 方法为每个不同的实例返回不同的结果。如果我们有默认的equals(),它的行为类似于 ==,它将内存中的所有实例视为不同的对象,我们没有任何问题。
但在我们之前的示例中,我们说过如果 Person 实例的年龄和名称匹配,我们希望将其视为相等。
Person person1 = new Person("Mike", 34);
Person person2 = new Person("Mike", 34);
System.out.println ( person1.equals(person2) ); --> will print true!
现在让我们创建一个映射来将这些实例存储为键,并将一些字符串作为对值
Map<Person, String> map = new HashMap();
map.put(person1, "1");
map.put(person2, "2");
在 Person 类中,我们没有覆盖 hashCode 方法,但我们已经覆盖了 equals 方法。由于默认的hashCode 为不同的java 实例提供不同的结果person1.hashCode() 和person2.hashCode() 有很大的机会产生不同的结果。
我们的地图可能以不同链接列表中的人结束。
这违背了HashMap的逻辑
一个HashMap不允许有多个相等的键!
但我们现在有,原因是从 Object Class 继承的默认 hashCode() 还不够。不是在我们覆盖了 Person 类的 equals() 方法之后。
这就是为什么我们必须在重写 equals 方法之后再重写 hashCode() 方法的原因。
现在让我们解决这个问题。让我们重写我们的hashCode() 方法来考虑equals() 考虑的相同字段,即age, name
public class Person {
private Integer age;
private String name;
..getters, setters, constructors
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age &&
name.equals(person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
现在让我们再次尝试将这些键保存在我们的 HashMap 中
Map<Person, String> map = new HashMap();
map.put(person1, "1");
map.put(person2, "2");
person1.hashCode() 和person2.hashCode() 肯定是一样的。假设它是 0。
HashMap 将转到存储桶 0 并在其中 LinkedList 将 person1 保存为值为“1”的键。对于第二个放置的 HashMap 足够智能,当它再次进入存储桶 0 以保存值为“2”的 person2 键时,它将看到那里已经存在另一个相等的键。所以它会覆盖以前的密钥。所以最终我们的 HashMap 中只会存在 person2 键。
现在我们符合 Hash Map 的规则,即不允许多个相等的键!