hashcode(),equal()方法深入解析


首先,想要明白hashCode的作用,必須要先知道Java中的集合。  

總的來說,Java中的集合(Collection)有兩類,一類是List,再有一類是Set。 前者集合內的元素是有序的,元素可以重復;后者元素無序,但元素不可重復。

那么這里就有一個比較嚴重的問題了:要想保證元素不重復,可兩個元素是否重復應該依據什么來判斷呢? 這就是Object.equals方法了。但是,如果每增加一個元素就檢查一次,那么當元素很多時,后添加到集合中的元素比較的次數就非常多了。 也就是說,如果集合中現在已經有1000個元素,那么第1001個元素加入集合時,它就要調用1000次equals方法。這顯然會大大降低效率。  

於是,Java采用了哈希表的原理。哈希(Hash)實際上是個人名,由於他提出一哈希算法的概念,所以就以他的名字命名了。 哈希算法也稱為散列算法,是將數據依特定算法直接指定到一個地址上。初學者可以這樣理解,hashCode方法實際上返回的就是對象存儲的物理地址(實際可能並不是)。  

這樣一來,當集合要添加新的元素時,先調用這個元素的hashCode方法,就一下子能定位到它應該放置的物理位置上。 如果這個位置上沒有元素,它就可以直接存儲在這個位置上,不用再進行任何比較了;如果這個位置上已經有元素了, 就調用它的equals方法與新元素進行比較,相同的話就不存了,不相同就散列其它的地址。 所以這里存在一個沖突解決的問題。這樣一來實際調用equals方法的次數就大大降低了,幾乎只需要一兩次。 

所以,Java對於eqauls方法和hashCode方法是這樣規定的:

1、如果兩個對象相同,那么它們的hashCode值一定要相同;

2、如果兩個對象的hashCode相同,它們並不一定相同(上面說的對象相同指的是用eqauls方法比較。)  

你當然可以不按要求去做了,但你會發現,相同的對象可以出現在Set集合中。同時,增加新元素的效率會大大下降。

hashcode這個方法是用來鑒定2個對象是否相等的。 那你會說,不是還有equals這個方法嗎? 不錯,這2個方法都是用來判斷2個對象是否相等的。但是他們是有區別的。 一般來講,equals這個方法是給用戶調用的,如果你想判斷2個對象是否相等,你可以重寫equals方法,然后在代碼中調用,就可以判斷他們是否相等 了。簡單來講,equals方法主要是用來判斷從表面上看或者從內容上看,2個對象是不是相等。

舉個例子,有個學生類,屬性只有姓名和性別,那么我們可以 認為只要姓名和性別相等,那么就說這2個對象是相等的。 hashcode方法一般用戶不會去調用,比如在hashmap中,由於key是不可以重復的,他在判斷key是不是重復的時候就判斷了hashcode 這個方法,而且也用到了equals方法。這里不可以重復是說equals和hashcode只要有一個不等就可以了!所以簡單來講,hashcode相 當於是一個對象的編碼,就好像文件中的md5,他和equals不同就在於他返回的是int型的,比較起來不直觀。我們一般在覆蓋equals的同時也要 覆蓋hashcode,讓他們的邏輯一致。舉個例子,還是剛剛的例子,如果姓名和性別相等就算2個對象相等的話,那么hashcode的方法也要返回姓名 的hashcode值加上性別的hashcode值,這樣從邏輯上,他們就一致了。 要從物理上判斷2個對象是否相等,用==就可以了。

在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 之前和之后)的狀態已經不同了。這也是要注意的。

知道以上這些能夠幫助你:

1. 進行更好的設計和開發。

2. 進行更好的測試案例開發。

3. 在面試過程中讓面試者對你的學識淵博感到滿意。

您可能感興趣的文章:

 

數組的特點:尋址容易,插入和刪除困難。

鏈表的特點是:尋址困難,插入和刪除容易。

ArrayList的底層實現就是通過動態數組來實現的,LinkedLIst底層實現就是通過鏈表來實現的,所以直接答出數組和鏈表的特點就ok

面試題: hashMap是怎樣實現key-value這樣鍵值對的保存?

HashMap中有一個內部類Entry,

 

?
1
2
3
4
5
6
7
static class Entry<k,v> implements Map.Entry<k,v> {
         final K key;
         V value;
         Entry<k,v> next;
         int hash;
         //.....
}</k,v></k,v></k,v>

主要有4個屬性,key ,hash,value,指向下一個節點的引用next ,看到這個實體類就明白了,在HashMap中存放的key-value實質是通過實體類Entry來保存的

 

面試題: hashMap的實現原理?

HashMap使用到的數據類型主要就是數組和鏈表,首先看原理圖

\

 

在hashMap的原理圖中,左邊是通過數組來存放鏈表的第一個節點,看懂這個圖這個問題就ok

面試題: hashMap的put過程?

面我們提到過Entry類里面有一個next屬性,作用是指向下一個Entry。比如說: 第一個鍵值對A進來,通過計算其key的hash得到的index=0,記做:Entry[0] = A。一會后又進來一個鍵值對B,通過計算其index也等於0,現在怎么辦?HashMap會這樣做:B.next = A,Entry[0] = B,如果又進來C,index也等於0,那么C.next = B,Entry[0] = C;這樣我們發現index=0的地方其實存取了A,B,C三個鍵值對,他們通過next這個屬性鏈接在一起。也就是說數組中存儲的是最后插入的元素。

 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public V put(K key, V value) {
     if (key == null )
         return putForNullKey(value);
     int hash = hash(key);
     int i = indexFor(hash, table.length);
     for (Entry<k,v> e = table[i]; e != null ; e = e.next) { //循環判斷插入的key是否已經存在,若存在就更新key對應的value
         Object k;
         if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
             V oldValue = e.value;
             e.value = value;
             e.recordAccess( this );
             return oldValue;
         }
     }
 
     modCount++;
     addEntry(hash, key, value, i); //key不存在,那么插入新的key-value
     return null ;
}</k,v>
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void addEntry( int hash, K key, V value, int bucketIndex) {
     if ((size >= threshold) && ( null != table[bucketIndex])) {
         resize( 2 * table.length);
         hash = ( null != key) ? hash(key) : 0 ;
         bucketIndex = indexFor(hash, table.length);
     }
 
     createEntry(hash, key, value, bucketIndex);
}
 
void createEntry( int hash, K key, V value, int bucketIndex) { //這個方法就驗證了上面說的<strong>數組中存儲的是最后插入的元素</strong>
     Entry<k,v> e = table[bucketIndex];
     table[bucketIndex] = new Entry<>(hash, key, value, e);
     size++;
}</k,v>

面試題: hashMap的get過程?
這個過程比較簡單,直接看代碼:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public V get(Object key) {
         if (key == null )
             return getForNullKey();
         int hash = hash(key.hashCode());
         //先定位到數組元素,再遍歷該元素處的鏈表
         for (Entry<k,v> e = table[indexFor(hash, table.length)];
              e != null ;
              e = e.next) {
             Object k;
             if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
                 return e.value;
         }
         return null ;
}</k,v>

面試題: hashMap存取的時候是如何定位數組下標的?
找到indexFor這個方法,hashMap就是通過這個方法獲取數組下標的:

 

 

?
1
2
3
static int indexFor( int h, int length) {
         return h & (length- 1 );
     }

通過key的hash值和數組長度求&,這意味着數組下標相同,並不表示hashCode相同。所以在Entry中存有一個hash值,在比較Entry的時候都是想比較hash值
面試題: hashMap中數組的初始化大小過程?

 

找到hashMap的構造方法:

 

?
1
2
3
4
5
6
7
8
9
10
public HashMap( int initialCapacity, float loadFactor) {
        ..... // Find a power of 2 >= initialCapacity
        int capacity = 1 ;
        while (capacity < initialCapacity)
            capacity <<= 1 //相當於capacity = capacity * 2
        this .loadFactor = loadFactor;
        threshold = ( int )(capacity * loadFactor);
        table = new Entry[capacity];
        init();
    }

從上面的代碼我們可以看出數組的初始大小並不是構造函數中的initialCapacity!!而是2的n次方
面試題: hashMap什么時候開始rehash?
在hashMap中有一個加載因子loadFactor,默認值是0.75,當數組的實際存入值的大小 > 數組的長度×loadFactor 時候就會rehash,重新創建一個新的表,將原表的映射到新表中,這個過程很費時。

 

 

9.list移除某個特定值

package com.bjs.test;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List; public class TestList {     public static void main(String[] args) {         List<String> list = new ArrayList<String>();         list.add("a");         list.add("a");         list.add("b");         list.add("b");         list.add("c");         list.add("c");         list.add("d");         list.add("e");         list.add("f");         list.remove("a");         Iterator<String> iter = list.iterator();         while (iter.hasNext()) {             String s = iter.next();             if (s.equals("a")) {                 iter.remove();             }         }         for (String ele : list) {             System.out.println(ele);         }     } }

map 的iterate 里面移除里面的key  再獲得map.get()會拿到值嗎
Student類可以做map的key值嗎 如果能請寫出Student的類
null可以做map的key嗎?

 

6.為什么String, Interger這樣的wrapper類適合作為鍵?

String, Interger這樣的wrapper類是final類型的,具有不可變性,而且已經重寫了equals()和hashCode()方法了。其他的wrapper類也有這個特點。不可變性是必要的,因為為了要計算hashCode(),就要防止鍵值改變,如果鍵值在放入時和獲取時返回不同的hashcode的話,那么就不能從HashMap中找到你想要的對象。 

 

7.ConcurrentHashMap和Hashtable的區別

Hashtable和ConcurrentHashMap有什么分別呢?它們都可以用於多線程的環境,但是當Hashtable的大小增加到一定的時候,性能會急劇下降,因為迭代時需要被鎖定很長的時間。因為ConcurrentHashMap引入了分割(segmentation),不論它變得多么大,僅僅需要鎖定map的某個部分,而其它的線程不需要等到迭代完成才能訪問map。簡而言之,在迭代的過程中,ConcurrentHashMap僅僅鎖定map的某個部分,而Hashtable則會鎖定整個map。

 

 

8.HashMap的遍歷

第一種:
  Map map = new HashMap();
  Iterator iter = map.entrySet().iterator();
  while (iter.hasNext()) {
  Map.Entry entry = (Map.Entry) iter.next();
  Object key = entry.getKey();
  Object val = entry.getValue();
  }
  效率高,以后一定要使用此種方式!
第二種:
  Map map = new HashMap();
  Iterator iter = map.keySet().iterator();
  while (iter.hasNext()) {
  Object key = iter.next();

Object val = map.get(key);
  }
  效率低,以后盡量少使用!

可是為什么第一種比第二種方法效率更高呢?

HashMap這兩種遍歷方法是分別對keyset及entryset來進行遍歷,但是對於keySet其實是遍歷了2次,一次是轉為iterator,一次就從hashmap中取出key所對於的value。而entryset只是遍歷了第一次,它把key和value都放到了entry中,即鍵值對,所以就快了。

1.HashMap與Hashtable的區別:

HashMap可以接受null鍵值和值,而Hashtable則不能。

Hashtable是線程安全的,通過synchronized實現線程同步。而HashMap是非線程安全的,但是速度比Hashtable快。

 

5.如果HashMap的大小超過了負載因子(load factor)定義的容量,怎么辦 

HashMap默認的負載因子大小為0.75,也就是說,當一個map填滿了75%的空間的時候,和其它集合類(如ArrayList等)一樣,將會創建原來HashMap大小的兩倍的數組,來重新調整map的大小,並將原來的對象放入新的數組中。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM