Hashcode的作用

chinayuan發表於2020-04-05
============================================================
如何理解hashCode的作用:
============================================================
以java.lang.Object來理解,JVM每new一個Object,它都會將這個Object丟到一個Hash雜湊表中去,這樣的話,下次做Object的比較或者取這個物件的時候,它會根據物件的hashcode再從Hash表中取這個物件。這樣做的目的是提高取物件的效率。具體過程是這樣:
1.new Object(),JVM根據這個物件的Hashcode值,放入到對應的Hash表對應的Key上,如果不同的物件確產生了相同的hash值,也就是發生了Hash key相同導致衝突的情況,那麼就在這個Hash key的地方產生一個連結串列,將所有產生相同hashcode的物件放到這個單連結串列上去,串在一起。
2.比較兩個物件的時候,首先根據他們的hashcode去hash表中找他的物件,當兩個物件的hashcode相同,那麼就是說他們這兩個物件放在Hash表中的同一個key上,那麼他們一定在這個key上的連結串列上。那麼此時就只能根據Object的equal方法來比較這個物件是否equal。當兩個物件的hashcode不同的話,肯定他們不能equal.

============================================================   
改寫equals時總是要改寫hashCode
============================================================
java.lnag.Object中對hashCode的約定:

   1. 在一個應用程式執行期間,如果一個物件的equals方法做比較所用到的資訊沒有被修改的話,則對該物件呼叫hashCode方法多次,它必須始終如一地返回同一個整數。
   2. 如果兩個物件根據equals(Object o)方法是相等的,則呼叫這兩個物件中任一物件的hashCode方法必須產生相同的整數結果。
   3. 如果兩個物件根據equals(Object o)方法是不相等的,則呼叫這兩個物件中任一個物件的hashCode方法,不要求產生不同的整數結果。但如果能不同,則可能提高雜湊表的效能。

   
有一個概念要牢記,兩個相等物件的equals方法一定為true, 但兩個hashcode相等的物件不一定是相等的物件。

所以hashcode相等只能保證兩個物件在一個HASH表裡的同一條HASH鏈上,繼而通過equals方法才能確定是不是同一物件,如果結果為true, 則認為是同一物件在插入,否則認為是不同物件繼續插入。

Object的程式碼:
public String toString () {
    return this.getClass().getName() + "@" + Integer.toHexString(this.hashCode());
}

public boolean equals (Object o) {
    return this == o;
}

/**
 * Answers an integer hash code for the receiver. Any two
 * objects which answer <code>true</code> when passed to
 * <code>.equals</code> must answer the same value for this
 * method.
 *
 * @author        OTI
 * @version        initial
 *
 * @return        int
 *                    the receiver's hash.
 *
 * @see            #equals
 */
public native int hashCode();


從上面我們可以看到是否很可能Object.hashCode就是代表記憶體地址。下面我們來證明hashcode是不是真的就是Object的記憶體地址呢?實際上,hashcode根本不能代表object的記憶體地址。
-----------------------------------------
Object.hashCode不可以代表記憶體地址
----------------------------------------

package com.tools;

import java.util.ArrayList;

/**
 * 此方法的作用是證明 java.lang.Object的hashcode 不是代表 物件所在記憶體地址。
 * 我產生了10000個物件,這10000個物件在記憶體中是不同的地址,但是實際上這10000個物件
 * 的hashcode的是完全可能相同的
 */
public class HashCodeMeaning {
    public static void main(String[] args) {
        ArrayList list =  new ArrayList();
        int numberExist=0;
       
        //證明hashcode的值不是記憶體地址
        for (int i = 0; i < 10000; i++) {
            Object obj=new Object();
            if (list.contains(obj.toString())) {
                System.out.println(obj.toString() +"  exists in the list. "+ i);
                numberExist++;
            }
            else {
                list.add(obj.toString());
            }
        }
       
        System.out.println("repetition number:"+numberExist);
        System.out.println("list size:"+list.size());
       
        //證明記憶體地址是不同的。
        numberExist=0;
        list.clear();
        for (int i = 0; i < 10000; i++) {
            Object obj=new Object();
            if (list.contains(obj)) {
                System.out.println(obj +"  exists in the list. "+ i);
                numberExist++;
            }
            else {
                list.add(obj);
            }
        }
       
        System.out.println("repetition number:"+numberExist);
        System.out.println("list size:"+list.size());
    }
}


==============================
看HashTable的原始碼非常有用:
==============================   

==============================  
HashCode方法使用簡介     
==============================  
Hash表資料結構常識:
一、雜湊表基於陣列。
二、缺點:基於陣列的,陣列建立後難以擴充套件。某些雜湊表被基本填滿時,效能下降得非常嚴重。
三、沒有一種簡便得方法可以以任何一種順序遍歷表中資料項。
四、如果不需要有序遍歷資料,並且可以提前預測資料量的大小,那麼雜湊表在速度和易用性方面是無與倫比的。
 
一、為什麼HashCode對於物件是如此的重要:
一個物件的HashCode就是一個簡單的Hash演算法的實現,雖然它和那些真正的複雜的Hash演算法相比還不能叫真正的演算法,它如何實現它,不僅僅是程式設計師的程式設計水平問題,
而是關係到你的物件在存取是效能的非常重要的關係.有可能,不同的HashCode可能會使你的物件存取產生,成百上千倍的效能差別.
先來看一下,在JAVA中兩個重要的資料結構:HashMap和Hashtable,雖然它們有很大的區別,如繼承關係不同,對value的約束條件(是否允許null)不同,以及執行緒安全性等有著特定的區別,但從實現原理上來說,它們是一致的.所以,我們只以Hashtable來說明:
在java中,存取資料的效能,一般來說當然是首推陣列,但是在資料量稍大的容器選擇中,Hashtable將有比資料效能更高的查詢速度.具體原因看下面的內容.
Hashtable在儲存資料時,一般先將該物件的HashCode和0x7FFFFFFF做與操作,因為一個物件的HashCode可以為負數,這樣操作後可以保證它為一個正整數.然後以Hashtable的長度取模,得到該物件在Hashtable中的索引.
index = (o.hashCode() & 0x7FFFFFFF)%hs.length;
這個物件就會直接放在Hashtable的每index位置,對於寫入,這和資料一樣,把一個物件放在其中的第index位置,但如果是查詢,經過同樣的演算法,Hashtable可以直接從第index取得這個物件,而陣列卻要做迴圈比較.所以對於資料量稍大時,Hashtable的查詢比資料具有更高的效能.
既然一個物件可以根據HashCode直接定位它在Hashtable中的位置,那麼為什麼Hashtable還要用key來做對映呢?這就是關係Hashtable效能問題的最重要的問題:Hash衝突.
常見的Hash衝突是不同物件最終產生了相同的索引,而一種非常甚至絕對少見的Hash衝突是,如果一組物件的個數大過了int範圍,而HashCode的長度只能在int範圍中,所以肯定要有同一組的元素有相同的HashCode,這樣無論如何他們都會有相同的索引.當然這種極端的情況是極少見的,可以暫不考慮,但是對於同的HashCode經過取模,則會產中相同的索引,或者不同的物件卻具有相同的HashCode,當然具有相同的索引.
所以對於索引相同的物件,在該index位置存放了多個值,這些值要想能正確區分,就要依靠key來識別.
事實上一個設計各好的HashTable,一般來說會比較平均地分佈每個元素,因為Hashtable的長度總是比實際元素的個數按一定比例進行自增(裝填因子一般為0.75)左右,這樣大多數的索引位置只有一個物件,而很少的位置會有幾個元素.所以Hashtable中的每個位置存放的是一個連結串列,對於只有一個物件是位置,連結串列只有一個首節點(Entry),Entry的next為null.然後有hashCode,key,value屬性儲存了該位置的物件的HashCode,key和value(物件本身),如果有相同索引的物件進來則會進入連結串列的下一個節點.如果同一個索引中有多個物件,根據HashCode和key可以在該連結串列中找到一個和查詢的key相匹配的物件.
從上面我看可以看到,對於HashMap和Hashtable的存取效能有重大影響的首先是應該使該資料結構中的元素儘量大可能具有不同的HashCode,雖然這並不能保證不同的HashCode產生不同的index,但相同的HashCode一定產生相同的index,從而影響產生Hash衝突.
對於一個象,如果具有很多屬性,把所有屬性都參與雜湊,顯然是一種笨拙的設計.因為物件的HashCode()方法幾乎無所不在地被自動呼叫,如equals比較,如果太多的物件參與了雜湊.
那麼需要的操作常數時間將會增加很大.所以,挑選哪些屬性參與雜湊絕對是一個程式設計水平的問題.
從實現來說,一般的HashCode方法會這樣:
return Attribute1.HashCode() Attribute1.HashCode()..[ super.HashCode()],我們知道,每次呼叫這個方法,都要重新對方法內的參與雜湊的物件重新計算一次它們的HashCode的運算,如果一個物件的屬性沒有改變,仍然要每次都進行計算,所以如果設定一個標記來快取當前的雜湊碼,只要當參與雜湊的物件改變時才重新計算,否則呼叫快取的hashCode,這可以從很大程度上提高效能.
預設的實現是將物件內部地址轉化為整數作為HashCode,這當然能保證每個物件具有不同的HasCode,因為不同的物件內部地址肯定不同(廢話),但java語言並不能讓程式設計師獲取物件內部地址,所以,讓每個物件產生不同的HashCode有著很多可研究的技術.
如果從多個屬性中取樣出能具有平均分佈的hashCode的屬性,這是一個效能和多樣性相矛盾的地方,如果所有屬性都參與雜湊,當然hashCode的多樣性將大大提高,但犧牲了效能,而如果只能少量的屬性取樣雜湊,極端情況會產生大量的雜湊衝突,如對"人"的屬性中,如果用性別而不是姓名或出生日期,那將只有兩個或幾個可選的hashcode值,將產生一半以上的雜湊衝突.所以如果可能的條件下,專門產生一個序列用來生成HashCode將是一個好的選擇(當然產生序列的效能要比所有屬性參與雜湊的效能高的情況下才行,否則還不如直接用所有屬性雜湊).
如何對HashCode的效能和多樣性求得一個平衡,可以參考相關演算法設計的書,其實並不一定要求非常的優秀,只要能盡最大可能減少雜湊值的聚集.重要的是我們應該記得HashCode對於我們的程式效能有著重要的影響,在程式設計時應該時時加以注意.
請記住:如果你想有效的使用HashMap,你就必須重寫在其的HashCode()。
還有兩條重寫HashCode()的原則:
不必對每個不同的物件都產生一個唯一的hashcode,只要你的HashCode方法使get()能夠得到put()放進去的內容就可以了。即“不為一原則”。生成hashcode的演算法儘量使hashcode的值分散一些, 不要很多hashcode都集中在一個範圍內,這樣有利於提高HashMap的效能。即“分散原則”。至於第二條原則的具體原因,有興趣者可以參考Bruce Eckel的《Thinking in Java》,
在那裡有對HashMap內部實現原理的介紹,這裡就不贅述了。
掌握了這兩條原則,你就能夠用好HashMap編寫自己的程式了。不知道大家注意沒有, java.lang.Object中提供的三個方法:clone(),equals()和hashCode()雖然很典型,但在很多情況下都不能夠適用,它們只是簡單的由物件的地址得出結果。這就需要我們在自己的程式中重寫它們,其實java類庫中也重寫了千千萬萬個這樣的方法。利用物件導向的多型性——覆蓋,Java的設計者很優雅的構建了Java的結構,也更加體現了Java是一門純OOP語言的特性。
Java提供的Collection和Map的功能是十分強大的,它們能夠使你的程式實現方式更為靈活,執行效率更高。希望本文能夠對大家更好的使用HashMap有所幫助。
 
hashcode理論與實踐:
有效和正確定義hashCode()和equals()
每個Java物件都有hashCode()和 equals()方法。許多類忽略(Override)這些方法的預設實施,以在物件例項之間提供更深層次的語義可比性。
雖然Java語言不直接支援關聯陣列。可以使用任何物件作為一個索引的陣列 -- 但在根Object類中使用hashCode()方法明確表示期望廣泛使用HashMap(及其前輩Hashtable)。理想情況下基於雜湊的容器提供有效插入和有效檢索;直接在物件模式中支援雜湊可以促進基於雜湊的容器的開發和使用。
定義物件的相等性
Object類有兩種方法來推斷物件的標識:equals()和hashCode()。一般來說,如果您忽略了其中一種,您必須同時忽略這兩種,因為兩者之間有必須維持的至關重要的關係。特殊情況是根據equals() 方法,如果兩個物件是相等的,它們必須有相同的hashCode()值(儘管這通常不是真的)。
特定類的equals()的語義在Implementer的左側定義;定義對特定類來說equals()意味著什麼是其設計工作的一部分。Object提供的預設實施簡單引用下面等式:
public boolean equals(Object obj) { return (this == obj); }
在這種預設實施情況下,只有它們引用真正同一個物件時這兩個引用才是相等的。同樣,Object提供的hashCode()的預設實施通過將物件的記憶體地址對映於一個整數值來生成。由於在某些架構上,地址空間大於int值的範圍,兩個不同的物件有相同的hashCode()是可能的。如果您忽略了hashCode(),您仍舊可以使用System.identityHashCode()方法來接入這類預設值。
忽略 equals() -- 簡單例項
預設情況下,equals()和hashCode()基於標識的實施是合理的,但對於某些類來說,它們希望放寬等式的定義。例如,Integer類定義equals() 與下面類似:
public boolean equals(Object obj) {
return (obj instanceof Integer
%26amp;%26amp; intValue() == ((Integer) obj).intValue());
}
在這個定義中,只有在包含相同的整數值的情況下這兩個Integer物件是相等的。結合將不可修改的Integer,這使得使用Integer作為HashMap中的關鍵字是切實可行的。這種基於值的Equal方法可以由Java類庫中的所有原始封裝類使用,如Integer、Float、Character和Boolean以及String(如果兩個String物件包含相同順序的字元,那它們是相等的)。由於這些類都是不可修改的並且可以實施hashCode()和equals(),它們都可以做為很好的雜湊關鍵字。
為什麼忽略 equals()和hashCode()?
如果Integer不忽略equals() 和 hashCode()情況又將如何?如果我們從未在HashMap或其它基於雜湊的集合中使用Integer作為關鍵字的話,什麼也不會發生。但是,如果我們在HashMap中使用這類Integer物件作為關鍵字,我們將不能夠可靠地檢索相關的值,除非我們在get()呼叫中使用與put()呼叫中極其類似的Integer例項。這要求確保在我們的整個程式中,只能使用對應於特定整數值的Integer物件的一個例項。不用說,這種方法極不方便而且錯誤頻頻。
Object的interface contract要求如果根據 equals()兩個物件是相等的,那麼它們必須有相同的hashCode()值。當其識別能力整個包含在equals()中時,為什麼我們的根物件類需要hashCode()?hashCode()方法純粹用於提高效率。Java平臺設計人員預計到了典型Java應用程式中基於雜湊的集合類(Collection Class)的重要性--如Hashtable、HashMap和HashSet,並且使用equals()與許多物件進行比較在計算方面非常昂貴。使所有Java物件都能夠支援 hashCode()並結合使用基於雜湊的集合,可以實現有效的儲存和檢索。
 
實施equals()和hashCode()的需求
實施equals()和 hashCode()有一些限制,Object檔案中列舉出了這些限制。特別是equals()方法必須顯示以下屬性:
Symmetry:兩個引用,a和 b,a.equals(b) if and only if b.equals(a)
Reflexivity:所有非空引用, a.equals(a)
Transitivity:If a.equals(b) and b.equals(c), then a.equals(c)
Consistency with hashCode():兩個相等的物件必須有相同的hashCode()值
Object的規範中並沒有明確要求equals()和 hashCode() 必須一致 -- 它們的結果在隨後的呼叫中將是相同的,假設“不改變物件相等性比較中使用的任何資訊。”這聽起來象“計算的結果將不改變,除非實際情況如此。”這一模糊宣告通常解釋為相等性和雜湊值計算應是物件的可確定性功能,而不是其它。
物件相等性意味著什麼?
人們很容易滿足Object類規範對equals() 和 hashCode() 的要求。決定是否和如何忽略equals()除了判斷以外,還要求其它。在簡單的不可修值類中,如Integer(事實上是幾乎所有不可修改的類),選擇相當明顯 -- 相等性應基於基本物件狀態的相等性。在Integer情況下,物件的唯一狀態是基本的整數值。
對於可修改物件來說,答案並不總是如此清楚。equals() 和hashCode() 是否應基於物件的標識(象預設實施)或物件的狀態(象Integer和String)?沒有簡單的答案 -- 它取決於類的計劃使用。對於象List和Map這樣的容器來說,人們對此爭論不已。Java類庫中的大多數類,包括容器類,錯誤出現在根據物件狀態來提供equals()和hashCode()實施。
如果物件的hashCode()值可以基於其狀態進行更改,那麼當使用這類物件作為基於雜湊的集合中的關鍵字時我們必須注意,確保當它們用於作為雜湊關鍵字時,我們並不允許更改它們的狀態。所有基於雜湊的集合假設,當物件的雜湊值用於作為集合中的關鍵字時它不會改變。如果當關鍵字在集合中時它的雜湊程式碼被更改,那麼將產生一些不可預測和容易混淆的結果。實踐過程中這通常不是問題 -- 我們並不經常使用象List這樣的可修改物件做為HashMap中的關鍵字。
一個簡單的可修改類的例子是Point,它根據狀態來定義equals()和hashCode()。如果兩個Point 物件引用相同的(x, y)座標,Point的雜湊值來源於x和y座標值的IEEE 754-bit表示,那麼它們是相等的。
對於比較複雜的類來說,equals()和hashCode()的行為可能甚至受到superclass或interface的影響。例如,List介面要求如果並且只有另一個物件是List,而且它們有相同順序的相同的Elements(由Element上的Object.equals() 定義),List物件等於另一個物件。hashCode()的需求更特殊--list的hashCode()值必須符合以下計算:
hashCode = 1;
Iterator i = list.iterator();
while (i.hasNext()) {
Object obj = i.next();
hashCode = 31*hashCode + (obj==null ? 0 : obj.hashCode());
}
不僅僅雜湊值取決於list的內容,而且還規定了結合各個Element的雜湊值的特殊演算法。(String類規定類似的演算法用於計算String的雜湊值。)
編寫自己的equals()和hashCode()方法
忽略預設的equals()方法比較簡單,但如果不違反對稱(Symmetry)或傳遞性(Transitivity)需求,忽略已經忽略的equals() 方法極其棘手。當忽略equals()時,您應該總是在equals()中包括一些Javadoc註釋,以幫助那些希望能夠正確擴充套件您的類的使用者。
作為一個簡單的例子,考慮以下類:
class A {
final B someNonNullField;
C someOtherField;
int someNonStateField;
}
我們應如何編寫該類的equals()的方法?這種方法適用於許多情況:
public boolean equals(Object other) {
// Not strictly necessary, but often a good optimization
if (this == other)
return true;
if (!(other instanceof A))
return false;
 
A otherA = (A) other;
return
(someNonNullField.equals(otherA.someNonNullField))
%26amp;%26amp; ((someOtherField == null)
? otherA.someOtherField == null
: someOtherField.equals(otherA.someOtherField)));
}
現在我們定義了equals(),我們必須以統一的方法來定義hashCode()。一種統一但並不總是有效的定義hashCode()的方法如下:
public int hashCode() { return 0; }
這種方法將生成大量的條目並顯著降低HashMaps的效能,但它符合規範。一個更合理的hashCode()實施應該是這樣:
public int hashCode() {
int hash = 1;
hash = hash * 31 + someNonNullField.hashCode();
hash = hash * 31
+ (someOtherField == null ? 0 : someOtherField.hashCode());
return hash;
}
注意:這兩種實施都降低了類狀態欄位的equals()或hashCode()方法一定比例的計算能力。根據您使用的類,您可能希望降低superclass的equals()或hashCode()功能一部分計算能力。對於原始欄位來說,在相關的封裝類中有helper功能,可以幫助建立雜湊值,如Float.floatToIntBits。
編寫一個完美的equals()方法是不現實的。通常,當擴充套件一個自身忽略了equals()的instantiable類時,忽略equals()是不切實際的,而且編寫將被忽略的equals()方法(如在抽象類中)不同於為具體類編寫equals()方法。關於例項以及說明的更詳細資訊請參閱Effective Java Programming Language Guide, Item 7 (參考資料) 。
有待改進?
將雜湊法構建到Java類庫的根物件類中是一種非常明智的設計折衷方法 -- 它使使用基於雜湊的容器變得如此簡單和高效。但是,人們對Java類庫中的雜湊演算法和物件相等性的方法和實施提出了許多批評。java.util中基於雜湊的容器非常方便和簡便易用,但可能不適用於需要非常高效能的應用程式。雖然其中大部分將不會改變,但當您設計嚴重依賴於基於雜湊的容器效率的應用程式時必須考慮這些因素,它們包括:
太小的雜湊範圍。使用int而不是long作為hashCode()的返回型別增加了雜湊衝突的機率。
糟糕的雜湊值分配。短strings和小型integers的雜湊值是它們自己的小整數,接近於其它“鄰近”物件的雜湊值。一個循規導矩(Well-behaved)的雜湊函式將在該雜湊範圍內更均勻地分配雜湊值。
無定義的雜湊操作。雖然某些類,如String和List,定義了將其Element的雜湊值結合到一個雜湊值中使用的雜湊演算法,但語言規範不定義將多個物件的雜湊值結合到新雜湊值中的任何批准的方法。我們在前面編寫自己的equals()和hashCode()方法中討論的List、String或例項類A使用的訣竅都很簡單,但算術上還遠遠不夠完美。類庫不提供任何雜湊演算法的方便實施,它可以簡化更先進的hashCode()實施的建立。
當擴充套件已經忽略了equals()的 instantiable類時很難編寫equals()。當擴充套件已經忽略了equals()的 instantiable類時,定義equals()的“顯而易見的”方式都不能滿足equals()方法的對稱或傳遞性需求。這意味著當忽略equals()時,您必須瞭解您正在擴充套件的類的結構和實施詳細資訊,甚至需要暴露基本類中的機密欄位,它違反了物件導向的設計的原則。
結束語
通過統一定義equals()和hashCode(),您可以提升類作為基於雜湊的集合中的關鍵字的使用性。有兩種方法來定義物件的相等性和雜湊值:基於標識,它是Object提供的預設方法;基於狀態,它要求忽略equals()和hashCode()。當物件的狀態更改時如果物件的雜湊值發生變化,確信當狀態作為雜湊關鍵字使用時您不允許更更改其狀態。




解析Java物件的equals()和hashCode()的使用:

在Java語言中,equals()和hashCode()兩個函式的使用是緊密配合的,你要是自己設計其中一個,就要設計另外一個。在多數情況 下,這兩個函式是不用考慮的,直接使用它們的預設設計就可以了。但是在一些情況下,這兩個函式最好是自己設計,才能確保整個程式的正常執行。最常見的是當 一個物件被加入收集物件(collection object)時,這兩個函式必須自己設計。更細化的定義是:如果你想將一個物件A放入另一個收集物件B裡,或者使用這個物件A為查詢一個元物件在收集對 象B裡位置的鑰匙,並支援是否容納,刪除收集物件B裡的元物件這樣的操作,那麼,equals()和hashCode()函式必須開發者自己定義。其他情 況下,這兩個函式是不需要定義的。

equals():
它是用於進行兩個物件的比較的,是物件內容的比較,當然也能用於進行物件參閱值的比較。什麼是物件參閱值的比較?就是兩個參閱變數的值得比較,我們 都知道參閱變數的值其實就是一個數字,這個數字可以看成是鑑別不同物件的代號。兩個物件參閱值的比較,就是兩個數字的比較,兩個代號的比較。這種比較是默 認的物件比較方式,在Object這個物件中,這種方式就已經設計好了。所以你也不用自己來重寫,浪費不必要的時間。

物件內容的比較才是設計equals()的真正目的,Java語言對equals()的要求如下,這些要求是必須遵循的。否則,你就不該浪費時間:
對稱性:如果x.equals(y)返回是“true”,那麼y.equals(x)也應該返回是“true”。
反射性:x.equals(x)必須返回是“true”。
類推性:如果x.equals(y)返回是“true”,而且y.equals(z)返回是“true”,那麼z.equals(x)也應該返回是“true”。
還有一致性:如果x.equals(y)返回是“true”,只要x和y內容一直不變,不管你重複x.equals(y)多少次,返回都是“true”。
任何情況下,x.equals(null),永遠返回是“false”;x.equals(和x不同型別的物件)永遠返回是“false”。
hashCode():
這 個函式返回的就是一個用來進行雜湊操作的整型代號,請不要把這個代號和前面所說的參閱變數所代表的代號弄混了。後者不僅僅是個代號還具有在記憶體中才查詢對 象的位置的功能。hashCode()所返回的值是用來分類物件在一些特定的收集物件中的位置。這些物件是HashMap, Hashtable, HashSet,等等。這個函式和上面的equals()函式必須自己設計,用來協助HashMap, Hashtable, HashSet,等等對自己所收集的大量物件進行搜尋和定位。

這些收集物件究竟如何工作的,想象每個元物件hashCode是一個箱子的 編碼,按照編碼,每個元物件就是根據hashCode()提供的代號歸入相應的箱子裡。所有的箱子加起來就是一個HashSet,HashMap,或 Hashtable物件,我們需要尋找一個元物件時,先看它的程式碼,就是hashCode()返回的整型值,這樣我們找到它所在的箱子,然後在箱子裡,每 個元物件都拿出來一個個和我們要找的物件進行對比,如果兩個物件的內容相等,我們的搜尋也就結束。這種操作需要兩個重要的資訊,一是物件的 hashCode(),還有一個是物件內容對比的結果。

hashCode()的返回值和equals()的關係如下:

如果x.equals(y)返回“true”,那麼x和y的hashCode()必須相等。
如果x.equals(y)返回“false”,那麼x和y的hashCode()有可能相等,也有可能不等。

為什麼這兩個規則是這樣的,原因其實很簡單,拿HashSet來說吧,HashSet可以擁有一個或更多的箱子,在同一個箱子中可以有一個 或更多的獨特元物件(HashSet所容納的必須是獨特的元物件)。這個例子說明一個元物件可以和其他不同的元物件擁有相同的hashCode。但是一個 元物件只能和擁有同樣內容的元物件相等。所以這兩個規則必須成立。

設計這兩個函式所要注意到的:
如果你設計的物件型別並不使用於收集性物件,那麼沒有必要自己再設計這兩個函式的處理方式。這是正確的物件導向設計方法,任何使用者一時用不到的功能,就先不要設計,以免給日後功能擴充套件帶來麻煩。

如果你在設計時想別出心裁,不遵守以上的兩套規則,那麼勸你還是不要做這樣想入非非的事。我還沒有遇到過哪一個開發者和我說設計這兩個函式要違背前面說的兩個規則,我碰到這些違反規則的情況時,都是作為設計錯誤處理。

當一個物件型別作為收集型物件的元物件時,這個物件應該擁有自己處理equals(),和/或處理hashCode()的設計,而且要遵守前面所說 的兩種原則。equals()先要查null和是否是同一型別。查同一型別是為了避免出現ClassCastException這樣的異常給丟出來。查 null是為了避免出現NullPointerException這樣的異常給丟出來。

如果你的物件裡面容納的資料過多,那麼這兩個函式 equals()和hashCode()將會變得效率低。如果物件中擁有無法serialized的資料,equals()有可能在操作中出現錯誤。想象 一個物件x,它的一個整型資料是transient型(不能被serialize成二進位制資料流)。然而equals()和hashCode()都有依靠 這個整型資料,那麼,這個物件在serialization之前和之後,是否一樣?答案是不一樣。因為serialization之前的整型資料是有效的 資料,在serialization之後,這個整型資料的值並沒有儲存下來,再重新由二進位制資料流轉換成物件後,兩者(物件在serialization 之前和之後)的狀態已經不同了。這也是要注意的。


============================================================      
有效和正確定義hashCode()和equals():
============================================================      

  級別:入門級


  每個Java物件都有hashCode()和 equals()方法。許多類忽略(Override)這些方法的預設實施,以在物件例項之間提供更深層次的語義可比性。在Java理念和實踐這一部分, Java開發人員Brian Goetz向您介紹在建立Java類以有效和準確定義hashCode()和equals()時應遵循的規則和指南。您可以在討論論壇與作者和其它讀者一同探討您對本文的看法。(您還可以點選本文頂部或底部的討論進入論壇。)
  
  雖然Java語言不直接支援關聯陣列 -- 可以使用任何物件作為一個索引的陣列 -- 但在根Object類中使用hashCode()方法明確表示期望廣泛使用HashMap(及其前輩Hashtable)。理想情況下基於雜湊的容器提供有效插入和有效檢索;直接在物件模式中支援雜湊可以促進基於雜湊的容器的開發和使用。

  定義物件的相等性

  Object類有兩種方法來推斷物件的標識:equals()和hashCode()。一般來說,如果您忽略了其中一種,您必須同時忽略這兩種,因為兩者之間有必須維持的至關重要的關係。特殊情況是根據equals() 方法,如果兩個物件是相等的,它們必須有相同的hashCode()值(儘管這通常不是真的)。

  特定類的equals()的語義在Implementer的左側定義;定義對特定類來說equals()意味著什麼是其設計工作的一部分。Object提供的預設實施簡單引用下面等式:

  public boolean equals(Object obj) { return (this == obj); }

  在這種預設實施情況下,只有它們引用真正同一個物件時這兩個引用才是相等的。同樣,Object提供的 hashCode()的預設實施通過將物件的記憶體地址對映於一個整數值來生成。由於在某些架構上,地址空間大於int值的範圍,兩個不同的物件有相同的 hashCode()是可能的。如果您忽略了hashCode(),您仍舊可以使用System.identityHashCode()方法來接入這類預設值。

    忽略 equals() -- 簡單例項

  預設情況下,equals()和hashCode()基於標識的實施是合理的,但對於某些類來說,它們希望放寬等式的定義。例如,Integer類定義equals() 與下面類似:

  public boolean equals(Object obj) {
  return (obj instanceof Integer
  && intValue() == ((Integer) obj).intValue());
  }

  在這個定義中,只有在包含相同的整數值的情況下這兩個Integer物件是相等的。結合將不可修改的 Integer,這使得使用Integer作為HashMap中的關鍵字是切實可行的。這種基於值的Equal方法可以由Java類庫中的所有原始封裝類使用,如Integer、Float、Character和Boolean以及String(如果兩個String物件包含相同順序的字元,那它們是相等的)。由於這些類都是不可修改的並且可以實施hashCode()和equals(),它們都可以做為很好的雜湊關鍵字。

  為什麼忽略 equals()和hashCode()?

  如果Integer不忽略equals() 和 hashCode()情況又將如何?如果我們從未在HashMap或其它基於雜湊的集合中使用Integer作為關鍵字的話,什麼也不會發生。但是,如果我們在HashMap中使用這類Integer物件作為關鍵字,我們將不能夠可靠地檢索相關的值,除非我們在get()呼叫中使用與put()呼叫中極其類似的Integer例項。這要求確保在我們的整個程式中,只能使用對應於特定整數值的Integer物件的一個例項。不用說,這種方法極不方便而且錯誤頻頻。

  Object的interface contract要求如果根據 equals()兩個物件是相等的,那麼它們必須有相同的hashCode()值。當其識別能力整個包含在equals()中時,為什麼我們的根物件類需要hashCode()?hashCode()方法純粹用於提高效率。Java平臺設計人員預計到了典型Java應用程式中基於雜湊的集合類(Collection Class)的重要性--如Hashtable、HashMap和HashSet,並且使用equals()與許多物件進行比較在計算方面非常昂貴。使所有Java物件都能夠支援 hashCode()並結合使用基於雜湊的集合,可以實現有效的儲存和檢索。

==============================
Go deep into HashCode:
==============================

為什麼HashCode對於物件是如此的重要?
一個物件的HashCode就是一個簡單的Hash演算法的實現,雖然它和那些真正的複雜的
Hash演算法相比還不能叫真正的演算法,但如何實現它,不僅僅是程式設計師的程式設計水平問題,
而是關係到你的物件在存取時效能的非常重要的問題.有可能,不同的HashCode可能
會使你的物件存取產生,成百上千倍的效能差別.

我們先來看一下,在JAVA中兩個重要的資料結構:HashMap和Hashtable,雖然它們有很
大的區別,如繼承關係不同,對value的約束條件(是否允許null)不同,以及執行緒安全性
等有著特定的區別,但從實現原理上來說,它們是一致的.所以,我們只以Hashtable來
說明:

在java中,存取資料的效能,一般來說當然是首推陣列,但是在資料量稍大的容器選擇中,
Hashtable將有比資料效能更高的查詢速度.具體原因看下面的內容.

Hashtable在儲存資料時,一般先將該物件的HashCode和0x7FFFFFFF做與操作,因為一個
物件的HashCode可以為負數,這樣操作後可以保證它為一個正整數.然後以Hashtable的
長度取模,得到該物件在Hashtable中的索引.

index = (o.hashCode() & 0x7FFFFFFF)%hs.length;
這個物件就會直接放在Hashtable的第index位置,對於寫入,這和陣列一樣,把一個物件
放在其中的第index位置,但如果是查詢,經過同樣的演算法,Hashtable可以直接從第index
取得這個物件,而陣列卻要做迴圈比較.所以對於資料量稍大時,Hashtable的查詢比資料
具有更高的效能.

既然可以根據HashCode直接定位物件在Hashtable中的位置,那麼為什麼Hashtable
要用key來做對映呢(為了一些思維有障礙的人能看到懂我加了一句話:而不是直接放value呢)
?這就是關係Hashtable效能問題的最重要的問題:Hash衝突.

常見的Hash衝突是不同物件最終產生了相同的索引,而一種非常甚至絕對少見的Hash衝突
是,如果一組物件的個數大過了int範圍,而HashCode的長度只能在int範圍中,所以肯定要
有同一組的元素有相同的HashCode,這樣無論如何他們都會有相同的索引.當然這種極端
的情況是極少見的,可以暫不考慮,但對於相同的HashCode經過取模,則會產中相同的索引,
或者不同的物件卻具有相同的HashCode,當然具有相同的索引.

所以對於索引相同的物件,在該index位置存放了多個物件,這些值要想能正確區分,就要依
靠key本身和hashCode來識別.

事實上一個設計各好的HashTable,一般來說會比較平均地分佈每個元素,因為Hashtable
的長度總是比實際元素的個數按一定比例進行自增(裝填因子一般為0.75)左右,這樣大多
數的索引位置只有一個物件,而很少的位置會有幾個物件.所以Hashtable中的每個位置存
放的是一個連結串列,對於只有一個物件的位置,連結串列只有一個首節點(Entry),Entry的next為
null.然後有hashCode,key,value. 屬性儲存了該位置的物件的HashCode,key和value(物件
本身),如果有相同索引的物件進來則會進入連結串列的下一個節點.如果同一個位置中有多個
物件,根據HashCode和key可以在該連結串列中找到一個和查詢的key相匹配的物件.

從上面我看可以看到,對於HashMap和Hashtable的存取效能有重大影響的首先是應該使該
資料結構中的元素儘量大可能具有不同的HashCode,雖然這並不能保證不同的HashCode
產生不同的index,但相同的HashCode一定產生相同的index,從而影響產生Hash衝突.

對於一個象,如果具有很多屬性,把所有屬性都參與雜湊,顯然是一種笨拙的設計.因為物件
的HashCode()方法幾乎無所不在地被自動呼叫,如equals比較,如果太多的物件參與了雜湊.
那麼需要的操作常數時間將會增加很大.所以,挑選哪些屬性參與雜湊絕對是一個程式設計水平
的問題.

從實現來說,一般的HashCode方法會這樣:

return Attribute1.HashCode() + Attribute2.HashCode()...[+super.HashCode()],

我們知道,每次呼叫這個方法,都要重新對方法內的參與雜湊的物件重新計算一次它們的
HashCode的運算,如果一個物件的屬性沒有改變,仍然要每次都進行計算,所以如果設定一
個標記來快取當前的雜湊碼,只要當參與雜湊的物件改變時才重新計算,否則呼叫快取的
hashCode,這可以從很大程度上提高效能.


預設的實現是將物件內部地址轉化為整數作為HashCode,這當然能保證每個物件具有不同
的HasCode,因為不同的物件內部地址肯定不同(廢話),但java語言並不能讓程式設計師獲取對
象內部地址,所以,讓每個物件產生不同的HashCode有著很多可研究的技術.

如何從多個屬性中取樣出能具有多樣性的hashCode的屬性,這是一個效能和多樣性相矛
盾的地方,如果所有屬性都參與雜湊,當然hashCode的多樣性將大大提高,但犧牲了效能,
而如果只有少量的屬性取樣雜湊,極端情況會產生大量的雜湊衝突,如對"人"的屬性中,如
果用性別而不是姓名或出生日期,那將只有兩個或幾個可選的hashcode值,將產生一半以上
的雜湊衝突.所以如果可能的條件下,專門產生一個序列用來生成HashCode將是一個好的選
擇(當然產生序列的效能要比所有屬性參與雜湊的效能高的情況下才行,否則還不如直接用
所有屬性雜湊).

如何對HashCode的效能和多樣性求得一個平衡,可以參考相關演算法設計的書,其實並不一定
要求非常的優秀,只要能盡最大可能減少雜湊值的聚集.重要的是我們應該記得HashCode對
於我們的程式效能有著生要的影響,在程式設計時應該時時加以注意.

相關文章