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);
}

根据Java文档中equals()方法的说明,任何实现都应遵守以下原则。

  • 对于任何对象x,x.equals(x)应返回true
  • 对于任何两个对象x和y,只有当y.equals(x)返回true时,x.equals(y)才应返回true
  • 对于多个对象x,y和z,如果x.equals(y)返回truey.equals(z)返回true,则x.equals(z)应返回true
  • 多次调用x.equals(y)应返回相同的结果,除非在equals()方法实现中使用的任何对象属性被修改。
  • Object类的equals()方法实现仅当两个引用指向同一对象时才返回true

Java hashCode()

Java Object hashCode() 是一個本機方法,返回對象的整數哈希碼值。 hashCode() 方法的一般契約是:

  • hashCode() 的多次調用應返回相同的整數值,除非修改了在 equals() 方法中使用的對象屬性。
  • 對象的哈希碼值在同一應用程序的多次執行中可以更改。
  • 如果兩個對象根據 equals() 方法相等,則它們的哈希碼必須相同。
  • 如果兩個對象根據 equals() 方法不相等,則它們的哈希碼不需要不同。它們的哈希碼值可能相等,也可能不相等。

equals() 和 hashCode() 方法的重要性

Java 中的 hashCode() 和 equals() 方法用於基於哈希表的實現中,用於存儲和檢索數據。我已在 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 和 setter 方法

	@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()方法都使用相同的字段進行計算,以保持它們的契約有效。如果您再次運行測試程序,將從映射中獲取對象,並且程序將打印10。我們還可以使用 Project Lombok 來自動生成equals和hashCode方法實現。

什麼是哈希碰撞

簡而言之,Java哈希表實現使用以下邏輯進行get和put操作。

  1. 首先,使用“key”哈希碼識別要使用的“桶”。
  2. 如果該桶中沒有具有相同哈希碼的對象,則將對象添加到put操作中,並對get操作返回null。
  3. 如果在桶中有其他具有相同哈希碼的對象,則“key”等方法將發揮作用。
    • 如果 equals() 返回 true 並且這是一個 put 操作,則對象值將被覆蓋。
    • 如果 equals() 返回 false 並且這是一個 put 操作,則新條目將被添加到桶中。
    • 如果 equals() 返回 true 並且這是一個 get 操作,則將返回對象值。
    • 如果 equals() 返回 false 並且這是一個 get 操作,則將返回 null。

下圖顯示了 HashMap 的桶項目以及它們的 equals() 和 hashCode() 之間的關係。 當兩個鍵具有相同的哈希碼時出現的現象稱為哈希碰撞。如果 hashCode() 方法未正確實現,將會有更多的哈希碰撞,並且映射條目將無法正確分佈,從而導致 get 和 put 操作的速度變慢。這就是在生成哈希碼時使用質數的原因,以便映射條目在所有桶中得到正確分佈。

如果我们不实现hashCode()和equals()会发生什么呢?

我们已经在上面看到,如果未实现hashCode(),我们将无法检索值,因为HashMap使用哈希码来找到桶并查找条目。如果只使用hashCode()而不实现equals(),同样也无法检索值,因为equals()方法将返回false。

实现equals()和hashCode()方法的最佳实践

  • 在equals()和hashCode()方法的实现中使用相同的属性,以确保它们的契约在更新任何属性时不会违反。
  • 最好使用不可变对象作为哈希表键,以便我们可以缓存哈希码,而不是在每次调用时计算它。这就是为什么String是哈希表键的良好选择,因为它是不可变的并且可以缓存哈希码值。
  • 实现hashCode()方法,以便发生最少的哈希冲突,并确保条目均匀分布在所有桶中。

你可以從我們的GitHub存儲庫下載完整的代碼。

Source:
https://www.digitalocean.com/community/tutorials/java-equals-hashcode