理解Java中hashCode的作用

============================================================ 
如何理解hashCode的作用:
============================================================ 

官方文檔定義:

hashcode方法返回該對象的哈希碼值。支持該方法是爲哈希表提供一些優點,例如,java.util.Hashtable 提供的哈希表。   
  
hashCode 的常規協定是:   
在 Java 應用程序執行期間,在同一對象上多次調用 hashCode 方法時,必須一致地返回相同的整數,前提是對象上 equals 比較中所用的信息沒有被修改。從某一應用程序的一次執行到同一應用程序的另一次執行,該整數無需保持一致。   
如果根據 equals(Object) 方法,兩個對象是相等的,那麼在兩個對象中的每個對象上調用 hashCode 方法都必須生成相同的整數結果。   
以下情況不 是必需的:如果根據 equals(java.lang.Object) 方法,兩個對象不相等,那麼在兩個對象中的任一對象上調用 hashCode 方法必定會生成不同的整數結果。但是,程序員應該知道,爲不相等的對象生成不同整數結果可以提高哈希表的性能。   
實際上,由 Object 類定義的 hashCode 方法確實會針對不同的對象返回不同的整數。(這一般是通過將該對象的內部地址轉換成一個整數來實現的,但是 JavaTM 編程語言不需要這種實現技巧。)   
  
當equals方法被重寫時,通常有必要重寫 hashCode 方法,以維護 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對
於我們的程序性能有着生要的影響,在程序設計時應該時時加以注意.


原文出處:http://blog.csdn.net/fenglibing/article/details/8905007

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章