爲什麼重寫equals就必須重寫hashcode?
我們必須先了解hash桶。水桶用來存放水,而hash桶用來存放多個hash值,hash算法負責將hash值分配到hash桶裏,而相同的hash值始終位於相同的桶內。當存儲元素時首先計算出hash值,然後找到對應的hash桶,把值放在該桶裏。查找元素時,同樣計算出hash值並找到對應的hash桶,隨後將桶內所有元素取出一一對比equals,即完成查找。
爲什麼要利用hash桶呢?這樣是爲了減少需要用equals比較的元素數量。想象下傳統的方案中我們將所有元素一一對比equals,而現在只需和一個hash桶內元素對比即可。
那如果不重寫hashcode,即可能出現equals相同的元素其hashcode不同。假如我們已經在set中插入了一個元素,現在插入一個跟它equals的元素,但由於兩者hashcode不同,因此被分配到不同的桶中,導致這個元素被添加進set,這違反了set的規則!
從上述可以看出,hashcode的作用是輔助equals的判斷,它主要考慮的是效率。
重寫equals
重寫equals具有許多陷阱,稍不小心就會違反下面這些原則:
- 自反性
- 相對性
- 傳遞性
- 永久性
- 任何非NULL對象不等於NULL
接下來我們先給出一個標準實現,然後一一解釋
@Override
public boolean equals(Object o) {
//自檢查
if(this == o )
return true;
//空檢查
if (o == null)
return false;
//類型檢查和轉換
if (getClass() != o.getClass())
return false;
Car car = (Car)o;
// 成員比較
return Objects.equals(name, car.name);
}
- 函數參數
函數參數一定要爲object,考慮如下情況:
Car a = new Car("my");
Object b = new Car ("my");
boolean answer = a.equals(b);
這裏answer將爲false。因此java的方法重載基於靜態類型,因此這裏調用的是Obeject.equals方法。當我們使用集合時會大量遇到這種情況。
自校驗
equals是一個被經常調用的基本方法,因此需要注意它的性能,自校驗可以極大的提高性能。空校驗
空校驗保證上述最後一條原則,也避免了NullPointerException類型檢查和轉換
這裏有兩種實現方式,我們採取的方式將導致所以子類equals返回false,這可能不是我們想要的結果,因爲有些子類僅添加功能而不增加成員變量,那麼它們應該允許和父類相等。對hibernate或者spring這些會在運行中產生子類的框架來講這點尤爲重要。因此,我們衍生出另一種做法採用instanceofif (!(o instanceof Car)) return false;
但需要注意的是這種做法會引起其他問題。例如SUV派生自Car同時添加了新的成員變量並覆蓋equals方法,那麼car.equals(suv)將返回true,而suv.equals(car)將返回false,這顯然違反了相對性。
當然我們可以在suv的equals中檢查object是否爲car來避免這個問題,但這種做法違反了傳遞性,比如:Car a = new Car("my"); SUV s1 = new SUV("my", "1"); SUV s2 = new SUV("my", "2");
a分別和s1以及s2滿足equals,但s1和s2不滿足。
所以這裏有兩種做法:使用getclass同時記住所有的子類和父類不能equals;使用instanceof方法同時設置equals爲final成員比較
基本類型使用==,引用類型使用equals,對於可能爲空的字段需要檢驗是否爲NULL。因此建議採用Objects.equals方法重寫hashcode
重寫hashcode需要注意幾點:
選擇字段
不能選擇equals函數沒有采用的字段,這樣可能導致兩個equals的對象其hashcode卻不相同!換句話即採用equals字段的子集。一致性
如果不小心將已經添加到hashset的對象作出了修改,由於hash值不會重新計算,hashset的內部結構也不會改變,因此接下來即使使用相equals的對象也無法查找到該元素!即使使用該元素本身也不行!所以我麼需要儘量使用不可變變量來計算hash值。計算過程
把某個非0的常數值,比如17,保存在一個名爲result的int類型的變量中。
對於對象中的每個域,做如下操作:
爲該域計算int類型的哈希值c:
如果該域是boolean類型,則計算(f?1:0)
如果該域是byte、char、short或者int類型,則計算(int)f
如果該域是long類型,則計算(int)(f^(f>>>32))
如果該域是float類型,則計算Float.floatToIntBits(f)
如果該域是double類型,則計算Double.doubleToLongBits(f),然後重複第三個步驟。
如果該域是一個對象引用,並且該類的equals方法通過遞歸調用equals方法來比較這個域,同樣爲這個域遞歸的調用hashCode,如果這個域爲null,則返回0。
如果該域是數組,則要把每一個元素當作單獨的域來處理,遞歸的運用上述規則,如果數組域中的每個元素都很重要,那麼可以使用Arrays.hashCode方法。
把上面計算得到的hash值c合併到result中