【注】本文譯自:Guide to hashCode() in Java | Baeldung
Java hashCode() 指南
1. 概述
雜湊是電腦科學的一個基本概念。
在 Java 中,高效的雜湊演算法支援一些最流行的集合,例如 HashMap(檢視這篇深入的 文章)和 HashSet。
在本教程中,我們將重點介紹 hashCode() 的工作原理、它如何在集合中處理以及如何正確實現它。
2. 在資料結構中使用 hashCode()
在某些情況下,最簡單的集合操作可能效率低下。
舉例來說,這會觸發線性搜尋,這對於大型列表效率非常低:
List<String> words = Arrays.asList("Welcome", "to", "Baeldung");
if (words.contains("Baeldung")) {
System.out.println("Baeldung is in the list");
}
Java 提供了許多資料結構來專門處理這個問題。 例如,幾個 Map 介面實現是 hash tables(雜湊表)。
使用雜湊表時,這些集合使用 hashCode() 方法計算給定鍵的雜湊值。然後他們在內部使用這個值來儲存資料,以便訪問操作更加高效。
3. 瞭解 hashCode() 的工作原理
簡而言之,hashCode() 返回一個由雜湊演算法生成的整數值。
相等的物件(根據它們的 equals())必須返回相同的雜湊碼。不同的物件不需要返回不同的雜湊碼。
hashCode() 的通用契約宣告:
- 在 Java 應用程式執行期間,只要在同一物件上多次呼叫它,hashCode() 必須始終返回相同的值,前提是物件上的 equals 比較中使用的資訊沒有被修改。這個值不需要從應用程式的一次執行到同一應用程式的另一次執行保持一致。
- 如果根據 equals(Object) 方法兩個物件相等,則對這兩個物件中的每一個呼叫 hashCode() 方法必須產生相同的值。
- 如果根據 equals(java.lang.Object) 方法兩個物件不相等,則對這兩個物件中的每一個呼叫 hashCode 方法不需要產生不同的整數結果。但是,開發人員應該意識到,為不相等的物件生成不同的整數結果可以提高雜湊表的效能。
“在合理可行的情況下,類 Object 定義的 hashCode() 方法確實為不同的物件返回不同的整數。(這通常通過將物件的內部地址轉換為整數來實現,但 JavaTM 程式語言不需要這種實現技術。)”
4. 一個簡單的 hashCode() 實現
一個完全符合上述約定的簡單 hashCode() 實現實際上非常簡單。
為了演示這一點,我們將定義一個示例 User 類來覆蓋該方法的預設實現:
public class User {
private long id;
private String name;
private String email;
// standard getters/setters/constructors
@Override
public int hashCode() {
return 1;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null)
return false;
if (this.getClass() != o.getClass())
return false;
User user = (User) o;
return id == user.id && (name.equals(user.name) && email.equals(user.email));
}
// getters and setters here
}
User 類為完全遵守各自合同的 equals() 和 hashCode() 提供自定義實現。更重要的是,讓 hashCode() 返回任何固定值並沒有什麼不合法的。
但是,這種實現將雜湊表的功能降級到基本上為零,因為每個物件都將儲存在同一個單個儲存桶中。
在這種情況下,雜湊表查詢是線性執行的,並沒有給我們帶來任何真正的優勢。我們將在第 7 節詳細討論。
5. 改進 hashCode() 實現
讓我們通過包含 User 類的所有欄位來改進當前的 hashCode() 實現,以便它可以為不相等的物件產生不同的結果:
@Override
public int hashCode() {
return (int) id * name.hashCode() * email.hashCode();
}
這個基本的雜湊演算法絕對比前一個好得多。這是因為它僅通過將 name 和 email 欄位的雜湊碼與 id 相乘來計算物件的雜湊碼。
一般來說,我們可以說這是一個合理的 hashCode() 實現,只要我們保持 equals() 實現與其一致。6. 標準 hashCode() 實現
我們用來計算雜湊碼的雜湊演算法越好,雜湊表的效能就越好。
讓我們看看一個“標準”實現,它使用兩個素數為計算出的雜湊碼新增更多的唯一性:
@Override
public int hashCode() {
int hash = 7;
hash = 31 * hash + (int) id;
hash = 31 * hash + (name == null ? 0 : name.hashCode());
hash = 31 * hash + (email == null ? 0 : email.hashCode());
return hash;
}
雖然我們需要了解 hashCode() 和 equals() 方法所扮演的角色,但我們不必每次都從頭開始實現它們。這是因為大多數 IDE 可以生成自定義 hashCode() 和 equals() 實現。從 Java 7 開始,我們有一個 Objects.hash() 實用方法來進行舒適的雜湊:
Objects.hash(name, email)
IntelliJ IDEA 生成以下實現:
@Override
public int hashCode() {
int result = (int) (id ^ (id >>> 32));
result = 31 * result + name.hashCode();
result = 31 * result + email.hashCode();
return result;
}
Eclipse 產生了這個:
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((email == null) ? 0 : email.hashCode());
result = prime * result + (int) (id ^ (id >>> 32));
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
除了上述基於 IDE 的 hashCode() 實現之外,還可以自動生成高效的實現,例如使用 Lombok.。
在這種情況下,我們需要在 pom.xml 中新增 lombok-maven 依賴:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-maven</artifactId>
<version>1.16.18.0</version>
<type>pom</type>
</dependency>
現在用@EqualsAndHashCode 註解 User 類就足夠了:
@EqualsAndHashCode
public class User {
// fields and methods here
}
同樣,如果我們希望 Apache Commons Lang 的 HashCodeBuilder 類為我們生成 hashCode() 實現,我們在 pom 檔案中包含 commons-lang Maven 依賴項:
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
hashCode() 可以這樣實現:
public class User {
public int hashCode() {
return new HashCodeBuilder(17, 37).
append(id).
append(name).
append(email).
toHashCode();
}
}
一般來說,在實現 hashCode() 時沒有通用的方法。我們強烈推薦閱讀 Joshua Bloch 的 Effective Java.。它提供了實現高效雜湊演算法的詳盡指南列表。
請注意,所有這些實現都以某種形式使用了數字 31。這是因為 31 有一個很好的屬性。它的乘法可以用按位移位代替,這比標準乘法要快:
31 * i == (i << 5) - i
7. 處理雜湊衝突
雜湊表的內在行為帶來了這些資料結構的一個相關方面:即使使用有效的雜湊演算法,兩個或多個物件可能具有相同的雜湊碼,即使它們不相等。因此,即使它們具有不同的雜湊表鍵,它們的雜湊碼也會指向同一個桶。
這種情況通常被稱為雜湊衝突,有多種處理方法,每種方法都有其優點和缺點。Java 的 HashMap 使用單獨的連結方法來處理衝突:
**“當兩個或多個物件指向同一個儲存桶時,它們只是儲存在一個連結串列中。在這種情況下,雜湊表是一個連結串列陣列,每個具有相同雜湊值的物件都附加到連結串列中的桶索引處。
在最壞的情況下,幾個桶會繫結一個連結串列,而對連結串列中物件的檢索將是線性執行的。”**
雜湊衝突方法簡單說明了高效實現 hashCode() 的重要性。
Java 8 為 HashMap 實現帶來了有趣的增強。如果桶大小超過特定閾值,則樹圖替換連結串列。這允許實現 O(logn) 查詢而不是悲觀 O(n)。
8. 建立一個簡單的應用程式
現在我們將測試標準 hashCode() 實現的功能。
讓我們建立一個簡單的 Java 應用程式,將一些 User 物件新增到 HashMap 並使用 SLF4J 在每次呼叫該方法時將訊息記錄到控制檯。
這是示例應用程式的入口點:
public class Application {
public static void main(String[] args) {
Map<User, User> users = new HashMap<>();
User user1 = new User(1L, "John", "john@domain.com");
User user2 = new User(2L, "Jennifer", "jennifer@domain.com");
User user3 = new User(3L, "Mary", "mary@domain.com");
users.put(user1, user1);
users.put(user2, user2);
users.put(user3, user3);
if (users.containsKey(user1)) {
System.out.print("User found in the collection");
}
}
}
這是 hashCode() 實現:
public class User {
// ...
public int hashCode() {
int hash = 7;
hash = 31 * hash + (int) id;
hash = 31 * hash + (name == null ? 0 : name.hashCode());
hash = 31 * hash + (email == null ? 0 : email.hashCode());
logger.info("hashCode() called - Computed hash: " + hash);
return hash;
}
}
這裡需要注意的是,每次在雜湊對映中儲存物件並使用 containsKey() 方法檢查時,都會呼叫 hashCode() 並將計算出的雜湊碼列印到控制檯:
[main] INFO com.baeldung.entities.User - hashCode() called - Computed hash: 1255477819
[main] INFO com.baeldung.entities.User - hashCode() called - Computed hash: -282948472
[main] INFO com.baeldung.entities.User - hashCode() called - Computed hash: -1540702691
[main] INFO com.baeldung.entities.User - hashCode() called - Computed hash: 1255477819
User found in the collection
9. 結論
很明顯,生成高效的 hashCode() 實現通常需要混合一些數學概念(即素數和任意數)、邏輯和基本數學運算。
無論如何,我們可以有效地實現 hashCode() ,而無需使用這些技術。我們只需要確保雜湊演算法為不相等的物件生成不同的雜湊碼,並且它與 equals() 的實現一致。
與往常一樣,本文中顯示的所有程式碼示例都可以在 GitHub 上找到。