在每個覆蓋了equals 方法的類中,都必須覆蓋 hashCode 方法。如果不這樣做的話,就會違反 hashCode 的通用約定,從而導致該類無法結合所有的給予散列的集合一起正常運作。這類集合包括 HashSet、HashMap,下面是Object 的通用規範:
- 在應用程序的執行期間,只要對象的 equals 方法的比較操作所用到的信息沒有被修改,那麽同一個對象的多次調用,hashCode 方法都必須返回同一個值。在一個應用程序和另一個應用程序的執行過程中,執行 hashCode 方法返回的值可以不相同。
- 如果兩個對象根據 equals 方法比較出來是相等的,那麽調用這兩個對象的 hashCode 方法都必須産生同樣的整數結果
- 如果兩個對象根據 equals 方法比較是不相等的,那麽調用這兩個對象的 hashCode 方法不一定要求其産生相同的結果,但是程序員應該知道,給不相等的對象産生截然不同的整數結果,有可能提高散列表的性能。
因沒有覆蓋 hashCode ,容易違反上面第二條的約定,即相等的對象必須擁有相同的 hashCode 散列值
根據類的 equals 方法,兩個截然不同的實例在邏輯上有可能是相等的。但是根據 Object 的 hashCode 方法來看,它們僅僅是兩個截然不同的對象而已。因此對象的 hashCode 方法返回兩個看起來是隨機的整數,而不是根據第二個約定所要求的那樣,返回兩個相等的整數。
例如下面這個例子
public class PhoneNumber {
int numbersOne; int numbersTwo; int numbersThree;
public PhoneNumber(int numbersOne, int numbersTwo, int numbersThree) { this.numbersOne = numbersOne; this.numbersTwo = numbersTwo; this.numbersThree = numbersThree; }
@Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof PhoneNumber)) return false; PhoneNumber that = (PhoneNumber) o; return Objects.equals(numbersOne, that.numbersOne) && Objects.equals(numbersTwo, that.numbersTwo) && Objects.equals(numbersThree, that.numbersThree); }
public static void main(String[] args) { Map numberMap = new HashMap(); numberMap.put(new PhoneNumber(707,867,5309),”Jenny”);
System.out.println(numberMap.get(new PhoneNumber(707,867,5309))); } }
此時,你可能希望 numberMap.get(new PhoneNumber(707,867,5309)) 會返回 “Jerry”,但它實際上返回的是null 。 這裏會涉及到兩個實例: 第一個實例是第一次添加進入的 PhoneNumber , 它會被添加到一個桶中。因爲沒有重寫 hashCode 方法,所以你取的時候是去另外一個桶中取出來的 PhoneNumber 實例。所以自然兩個實例不相等,因爲 HashMap 有一項優化,可以將與每個項相關聯的散列碼緩存起來,如果散列碼不匹配,也就不再去檢驗對象的等同性。
修正這個問題非常的簡單,只要提供一個相等的散列碼就可以了
@Override public int hashCode() { return 42; }
上面這個 hashCode 方法是合法的。因爲它確保了相等的對象總是具有同樣的散列碼。但是它也極爲惡劣,因爲每個對象都具有相同的散列碼。因此,多個具有相同散列碼的 HashMap 就會彼此連在一起形成鏈表。它使得本該以線性時間運行的程序編程了以平方級的時間運行。
一個好的散列通常是 “爲不相等的對象産生不相等的散列碼”。這正是 hashCode 約定中的第三條含義。理想情況下,散列函數應該把集合中不相等的實例均勻地分布到所有可能的 int 值上。下面是一種簡單的解決辦法:
- 聲明一個 int 變量並命名爲 result,將它初始化爲對象中的第一個關鍵域散列碼 c
- 對象中剩下的每一個關鍵域 f 都完成一下步驟:爲該域計算 int 類型的散列碼 c:如果該域是基本類型,則計算 Type.hashCode(f),這裏的 Type 是集裝箱基本類型的類,與 f 的類型相對應如果該域是一個對象引用,並且該類的 equals 方法通過遞歸地調用 equals 的方式來比較這個域,則同樣爲這個域遞歸地調用 hashCode 。如果爲null ,則返回0如果該域是一個數組,則要把每一個元素當作單獨的域來處理。也就是說,遞歸地應用上述規則,對每個重要的元素計算一個散列碼,然後根據步驟2 . b中的做法把這些散列值組合起來。如果數組域中沒有重要的元素,可以使用一個常量,但最好不要用0。如果數組域中的所有元素都很重要,可以使用 Arrays.hashCode 方法。按照 下面的公式,把散列碼 c 合並到 result 中。1result = 31 * result + c;
- 返回result
寫完了之後,還要進行驗證,相等的實例是否具有相同的散列碼,可以把上述解決辦法用到 PhoneNumber 中
@Override public int hashCode() { int result = Integer.hashCode(numbersOne); result = 31 * result + Integer.hashCode(numbersTwo); result = 31 * result + Integer.hashCode(numbersThree); return result; }
雖然上述給出了 hashCode 實現,但它不是最先進的。它們的質量堪比 Java 平台類庫提供的散列函數。這些方法對于大多數應用程序而言已經足夠了。
Objects 類有一個靜態方法,它帶有任意數量的對象,並爲它們返回一個散列碼。這個方法名爲 hash 。你只需要一行代碼就可以編寫它的 hashCode 方法。它們的質量也是很高的,但是,它的運行速度相對慢一些,因爲它們會引發數組的創建,以便傳入數目可變的參數,如果參數中有基本類型,還需要裝箱和拆箱。例如:
@Override public int hashCode(){ return Objects.hash(numbersOne,numbersTwo,numbersThree); }
如果一個類是不可變的,並且計算 hashCode 的開銷也大,那麽應該把它換存在對象內部,而不是每次請求都重新創建 hashCode。你可以選擇 “延遲初始化” 的散列碼。即一直到 hashCode 被第一次使用的時候進行初始化。如下:
private int hashCode;
@Override public int hashCode() { int result = hashCode; if(result == 0){ result = Integer.hashCode(numbersOne); result = 31 * result + Integer.hashCode(numbersTwo); result = 31 * result + Integer.hashCode(numbersThree); hashCode = result; } return result; }
當你要重寫對象的 hashCode 方法時,下面這兩個約定我希望你能遵守:
- 不要對 hashCode 方法的返回值做具體的規定,因此客戶端無法理所當然地依賴它;這樣可以爲修改提供靈活性。
- 不要試圖從散列碼計算中排除掉一個對象的關鍵域來提高性能。
總而言之,每當覆蓋 equals 方法時都必須覆蓋 hashCode。否則程序將無法正確運行。hashCode 方法必須遵守 Object 規定的通用約定,並且一起完成一定的工作。將不相等的散列碼分配給不相等的實例。這個很容易實現,但是如果不想那麽費力,可以直接使用 eclipse 或者 Idea 提供的 AutoValue 自動生成就可以了。