[Java提高篇之hashCode](http://www.importnew.com/20381.html)
原文出處:?[chenssy](http://www.cnblogs.com/chenssy/p/3651218.html)
在前面三篇博文中LZ講解了([HashMap](http://www.cnblogs.com/chenssy/p/3521565.html)、[HashSet](http://www.cnblogs.com/chenssy/p/3621851.html)、[HashTable](http://www.cnblogs.com/chenssy/p/3643886.html)),在其中LZ不斷地講解他們的put和get方法,在這兩個方法中計算key的hashCode應該是最重要也是最精華的部分,所以下面LZ揭開hashCode的“神秘”面紗。
## hashCode的作用
要想了解一個方法的內在原理,我們首先需要明白它是干什么的,也就是這個方法的作用。在講解數組時([java提高篇(十八)——數組](http://www.cnblogs.com/chenssy/p/3463719.html)),我們提到數組是java中效率最高的數據結構,但是“最高”是有前提的。第一我們需要知道所查詢數據的所在位置。第二:如果我們進行迭代查找時,數據量一定要小,對于大數據量而言一般推薦集合。
在Java集合中有兩類,一類是List,一類是Set他們之間的區別就在于List集合中的元素師有序的,且可以重復,而Set集合中元素是無序不可重復的。對于List好處理,但是對于Set而言我們要如何來保證元素不重復呢?通過迭代來equals()是否相等。數據量小還可以接受,當我們的數據量大的時候效率可想而知(當然我們可以利用算法進行優化)。比如我們向HashSet插入1000數據,難道我們真的要迭代1000次,調用1000次equals()方法嗎?hashCode提供了解決方案。怎么實現?我們先看hashCode的源碼(Object)。
|
1
|
`public` `native` `int` `hashCode();`
|
它是一個本地方法,它的實現與本地機器有關,這里我們暫且認為他返回的是對象存儲的物理位置(實際上不是,這里寫是便于理解)。當我們向一個集合中添加某個元素,集合會首先調用hashCode方法,這樣就可以直接定位它所存儲的位置,若該處沒有其他元素,則直接保存。若該處已經有元素存在,就調用equals方法來匹配這兩個元素是否相同,相同則不存,不同則散列到其他位置(具體情況請參考(Java提高篇()—–HashMap))。這樣處理,當我們存入大量元素時就可以大大減少調用equals()方法的次數,極大地提高了效率。
所以**hashCode在上面扮演的角色為尋域**(尋找某個對象在集合中區域位置)。hashCode可以將集合分成若干個區域,每個對象都可以計算出他們的hash碼,可以將hash碼分組,每個分組對應著某個存儲區域,根據一個對象的hash碼就可以確定該對象所存儲區域,這樣就大大減少查詢匹配元素的數量,提高了查詢效率。
## hashCode對于一個對象的重要性
hashCode重要么?不重要,對于List集合、數組而言,他就是一個累贅,但是對于HashMap、HashSet、HashTable而言,它變得異常重要。所以在使用HashMap、HashSet、HashTable時一定要注意hashCode。對于一個對象而言,其hashCode過程就是一個簡單的Hash算法的實現,其實現過程對你實現對象的存取過程起到非常重要的作用。
在前面LZ提到了HashMap和HashTable兩種數據結構,雖然他們存在若干個區別,但是他們的實現原理是相同的,這里我以HashTable為例闡述hashCode對于一個對象的重要性。
一個對象勢必會存在若干個屬性,如何選擇屬性來進行散列考驗著一個人的設計能力。如果我們將所有屬性進行散列,這必定會是一個糟糕的設計,因為對象的hashCode方法無時無刻不是在被調用,如果太多的屬性參與散列,那么需要的操作數時間將會大大增加,這將嚴重影響程序的性能。但是如果較少屬相參與散列,散列的多樣性會削弱,會產生大量的散列“沖突”,除了不能夠很好的利用空間外,在某種程度也會影響對象的查詢效率。其實這兩者是一個矛盾體,散列的多樣性會帶來性能的降低。
那么如何對對象的hashCode進行設計,LZ也沒有經驗。從網上查到了這樣一種解決方案:設置一個緩存標識來緩存當前的散列碼,只有當參與散列的對象改變時才會重新計算,否則調用緩存的hashCode,這樣就可以從很大程度上提高性能。
在HashTable計算某個對象在table[]數組中的索引位置,其代碼如下:
|
1
|
`int` `index = (hash &` `0x7FFFFFFF``) % tab.length;`
|
為什么要&0x7FFFFFFF?因為某些對象的hashCode可能會為負值,與0x7FFFFFFF進行與運算可以確保index為一個正數。通過這步我可以直接定位某個對象的位置,所以從理論上來說我們是完全可以利用hashCode直接定位對象的散列表中的位置,但是為什么會存在一個key-value的鍵值對,利用key的hashCode來存入數據而不是直接存放value呢?這就關系HashTable性能問題的最重要的問題:Hash沖突!
我們知道沖突的產生是由于不同的對象產生了相同的散列碼,假如我們設計對象的散列碼可以確保99.999999999%的不重復,但是有一種絕對且幾乎不可能遇到的沖突你是絕對避免不了的。我們知道hashcode返回的是int,它的值只可能在int范圍內。如果我們存放的數據超過了int的范圍呢?這樣就必定會產生兩個相同的index,這時在index位置處會存儲兩個對象,我們就可以利用key本身來進行判斷。所以具有相索引的對象,在該index位置處存在多個對象,我們必須依靠key的hashCode和key本身來進行區分。
## hashCode與equals
在Java中hashCode的實現總是伴隨著equals,他們是緊密配合的,你要是自己設計了其中一個,就要設計另外一個。當然在多數情況下,這兩個方法是不用我們考慮的,直接使用默認方法就可以幫助我們解決很多問題。但是在有些情況,我們必須要自己動手來實現它,才能確保程序更好的運作。
對于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,我們應該遵循如下規則:
????? 1.?在一個應用程序執行期間,如果一個對象的equals方法做比較所用到的信息沒有被修改的話,則對該對象調用hashCode方法多次,它必須始終如一地返回同一個整數。
????? 2.?如果兩個對象根據equals(Object o)方法是相等的,則調用這兩個對象中任一對象的hashCode方法必須產生相同的整數結果。
????? 3.?如果兩個對象根據equals(Object o)方法是不相等的,則調用這兩個對象中任一個對象的hashCode方法,不要求產生不同的整數結果。但如果能不同,則可能提高散列表的性能。
至于兩者之間的關聯關系,我們只需要記住如下即可:
如果x.equals(y)返回“true”,那么x和y的hashCode()必須相等。
如果x.equals(y)返回“false”,那么x和y的hashCode()有可能相等,也有可能不等。
理清了上面的關系我們就知道他們兩者是如何配合起來工作的。先看下圖:
[](http://images.cnitblog.com/blog/381060/201404/080846536063051.png "2014040701_thumb2")
整個處理流程是:
1、判斷兩個對象的hashcode是否相等,若不等,則認為兩個對象不等,完畢,若相等,則比較equals。
2、若兩個對象的equals不等,則可以認為兩個對象不等,否則認為他們相等。
實例:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
`public` `class` `Person {`
`private` `int` `age;`
`private` `int` `sex;` `//0:男,1:女`
`private` `String name;`
`private` `final` `int` `PRIME =` `37``;`
`Person(``int` `age ,``int` `sex ,String name){`
`this``.age = age;`
`this``.sex = sex;`
`this``.name = name;`
`}`
`/** 省略getter、setter方法 **/`?
`@Override`
`public` `int` `hashCode() {`
`System.out.println(``"調用hashCode方法..........."``);`
`int` `hashResult =` `1``;`
`hashResult = (hashResult + Integer.valueOf(age).hashCode() + Integer.valueOf(sex).hashCode()) * PRIME;`
`hashResult = PRIME * hashResult + ((name ==` `null``) ?` `0` `: name.hashCode());`
`System.out.println(``"name:"``+name +``" hashCode:"` `+ hashResult);`
`return` `hashResult;`
`}?`
`/**`
`* 重寫hashCode()`
`*/`
`public` `boolean` `equals(Object obj) {`
`System.out.println(``"調用equals方法..........."``);`
`if``(obj ==` `null``){`
`return` `false``;`
`}`
`if``(obj.getClass() !=` `this``.getClass()){`
`return` `false``;`
`}`
`if``(``this` `== obj){`
`return` `true``;`
`}`
`Person person = (Person) obj;`
`if``(getAge() != person.getAge() || getSex()!= person.getSex()){`
`return` `false``;`
`}???`
`if``(getName() !=` `null``){`
`if``(!getName().equals(person.getName())){`
`return` `false``;`
`}??`
`}??`
`else` `if``(person !=` `null``){?`
`return` `false``;?`
`}???`
`return` `true``;?`
`}`
`}`
|
該Bean為一個標準的Java Bean,重新實現了hashCode方法和equals方法。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
`public` `class` `Main` `extends` `JPanel {`
`public` `static` `void` `main(String[] args) {`
`Set<Person> set =` `new` `HashSet<Person>();`
`Person p1 =` `new` `Person(``11``,` `1``,` `"張三"``);`
`Person p2 =` `new` `Person(``12``,` `1``,` `"李四"``);`
`Person p3 =` `new` `Person(``11``,` `1``,` `"張三"``);`
`Person p4 =` `new` `Person(``11``,` `1``,` `"李四"``);`
`//只驗證p1、p3`
`System.out.println(``"p1 == p3? :"` `+ (p1 == p3));???????`
`System.out.println(``"p1.equals(p3)?:"``+p1.equals(p3));`
`System.out.println(``"-----------------------分割線--------------------------"``);`
`set.add(p1);`
`set.add(p2);`
`set.add(p3);`
`set.add(p4);??????? System.out.println(``"set.size()="``+set.size());`
`}`
`}`
|
運行結果如下:
[](http://images.cnitblog.com/blog/381060/201404/080846546221108.png "2014040702_thumb")
從上圖可以看出,程序調用四次hashCode方法,一次equals方法,其set的長度只有3。add方法運行流程完全符合他們兩者之間的處理流程。
更多請關注:
????? >>>>>>>>>[Java提高篇(十三)——equals()](http://www.cnblogs.com/chenssy/p/3416195.html)
????? >>>>>>>>>[Java提高篇(二三)——HashMap](http://www.cnblogs.com/chenssy/p/3521565.html)
????? >>>>>>>>>[Java提高篇(二四)——HashSet](http://www.cnblogs.com/chenssy/p/3621851.html)
????? >>>>>>>>>[Java提高篇(二五)——HashTable](http://www.cnblogs.com/chenssy/p/3643886.html)
- JVM
- 深入理解Java內存模型
- 深入理解Java內存模型(一)——基礎
- 深入理解Java內存模型(二)——重排序
- 深入理解Java內存模型(三)——順序一致性
- 深入理解Java內存模型(四)——volatile
- 深入理解Java內存模型(五)——鎖
- 深入理解Java內存模型(六)——final
- 深入理解Java內存模型(七)——總結
- Java內存模型
- Java內存模型2
- 堆內內存還是堆外內存?
- JVM內存配置詳解
- Java內存分配全面淺析
- 深入Java核心 Java內存分配原理精講
- jvm常量池
- JVM調優總結
- JVM調優總結(一)-- 一些概念
- JVM調優總結(二)-一些概念
- VM調優總結(三)-基本垃圾回收算法
- JVM調優總結(四)-垃圾回收面臨的問題
- JVM調優總結(五)-分代垃圾回收詳述1
- JVM調優總結(六)-分代垃圾回收詳述2
- JVM調優總結(七)-典型配置舉例1
- JVM調優總結(八)-典型配置舉例2
- JVM調優總結(九)-新一代的垃圾回收算法
- JVM調優總結(十)-調優方法
- 基礎
- Java 征途:行者的地圖
- Java程序員應該知道的10個面向對象理論
- Java泛型總結
- 序列化與反序列化
- 通過反編譯深入理解Java String及intern
- android 加固防止反編譯-重新打包
- volatile
- 正確使用 Volatile 變量
- 異常
- 深入理解java異常處理機制
- Java異常處理的10個最佳實踐
- Java異常處理手冊和最佳實踐
- Java提高篇——對象克隆(復制)
- Java中如何克隆集合——ArrayList和HashSet深拷貝
- Java中hashCode的作用
- Java提高篇之hashCode
- 常見正則表達式
- 類
- 理解java類加載器以及ClassLoader類
- 深入探討 Java 類加載器
- 類加載器的工作原理
- java反射
- 集合
- HashMap的工作原理
- ConcurrentHashMap之實現細節
- java.util.concurrent 之ConcurrentHashMap 源碼分析
- HashMap的實現原理和底層數據結構
- 線程
- 關于Java并發編程的總結和思考
- 40個Java多線程問題總結
- Java中的多線程你只要看這一篇就夠了
- Java多線程干貨系列(1):Java多線程基礎
- Java非阻塞算法簡介
- Java并發的四種風味:Thread、Executor、ForkJoin和Actor
- Java中不同的并發實現的性能比較
- JAVA CAS原理深度分析
- 多個線程之間共享數據的方式
- Java并發編程
- Java并發編程(1):可重入內置鎖
- Java并發編程(2):線程中斷(含代碼)
- Java并發編程(3):線程掛起、恢復與終止的正確方法(含代碼)
- Java并發編程(4):守護線程與線程阻塞的四種情況
- Java并發編程(5):volatile變量修飾符—意料之外的問題(含代碼)
- Java并發編程(6):Runnable和Thread實現多線程的區別(含代碼)
- Java并發編程(7):使用synchronized獲取互斥鎖的幾點說明
- Java并發編程(8):多線程環境中安全使用集合API(含代碼)
- Java并發編程(9):死鎖(含代碼)
- Java并發編程(10):使用wait/notify/notifyAll實現線程間通信的幾點重要說明
- java并發編程-II
- Java多線程基礎:進程和線程之由來
- Java并發編程:如何創建線程?
- Java并發編程:Thread類的使用
- Java并發編程:synchronized
- Java并發編程:Lock
- Java并發編程:volatile關鍵字解析
- Java并發編程:深入剖析ThreadLocal
- Java并發編程:CountDownLatch、CyclicBarrier和Semaphore
- Java并發編程:線程間協作的兩種方式:wait、notify、notifyAll和Condition
- Synchronized與Lock
- JVM底層又是如何實現synchronized的
- Java synchronized詳解
- synchronized 與 Lock 的那點事
- 深入研究 Java Synchronize 和 Lock 的區別與用法
- JAVA編程中的鎖機制詳解
- Java中的鎖
- TreadLocal
- 深入JDK源碼之ThreadLocal類
- 聊一聊ThreadLocal
- ThreadLocal
- ThreadLocal的內存泄露
- 多線程設計模式
- Java多線程編程中Future模式的詳解
- 原子操作(CAS)
- [譯]Java中Wait、Sleep和Yield方法的區別
- 線程池
- 如何合理地估算線程池大小?
- JAVA線程池中隊列與池大小的關系
- Java四種線程池的使用
- 深入理解Java之線程池
- java并發編程III
- Java 8并發工具包漫游指南
- 聊聊并發
- 聊聊并發(一)——深入分析Volatile的實現原理
- 聊聊并發(二)——Java SE1.6中的Synchronized
- 文件
- 網絡
- index
- 內存文章索引
- 基礎文章索引
- 線程文章索引
- 網絡文章索引
- IOC
- 設計模式文章索引
- 面試
- Java常量池詳解之一道比較蛋疼的面試題
- 近5年133個Java面試問題列表
- Java工程師成神之路
- Java字符串問題Top10
- 設計模式
- Java:單例模式的七種寫法
- Java 利用枚舉實現單例模式
- 常用jar
- HttpClient和HtmlUnit的比較總結
- IO
- NIO
- NIO入門
- 注解
- Java Annotation認知(包括框架圖、詳細介紹、示例說明)