java的HashCode equals == 以及hashMap底層實現深入理解
1.==等號
對比物件例項的記憶體地址(也即物件例項的ID),來判斷是否是同一物件例項;又可以說是判斷物件例項是否物理相等;(參見:http://kakajw.iteye.com/blog/935226)
2.equals
檢視底層object的equals方法 public boolean equals(Object obj) { return (this == obj); }
當物件所屬的類沒有重寫根類Object的equals()方法時 呼叫==判斷 即物理相等
當物件所屬的類重寫equals()方法(可能因為需要自己特有的“邏輯相等”概念)
equals()判斷的根據就因具體實現而異:
如String類重寫equals()來判斷字串的值是否相等
public boolean equals(Object anObject) { if (this == anObject) { return true; } if (anObject instanceof String) { String anotherString = (String) anObject; int n = value.length; if (n == anotherString.value.length) { char v1[] = value; char v2[] = anotherString.value; int i = 0; while (n-- != 0) { if (v1[i] != v2[i]) return false; i++; } return true; } } return false; }
首先,如果兩個String引用 實際上是指向同一個String物件時(物理相等)則返回true
如果物理不相等,還要判斷:通過遍歷兩個物件底層的char陣列,如果每一個字元都完全相等,則認為這兩個物件是相等的(邏輯相等)
3.hashCode()
java.lang.Object類的原始碼中 方法是native的
<span style="font-size:18px;">public native int hashCode();</span>
方法的計算依賴於物件例項的ID(記憶體地址),每個Object物件的hashCode都是唯一的通過將該物件的內部地址轉換成一個整數來實現的 ,toString方法也會用到這個hashCode
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
如果覆寫了hashCode方法,則情況不一樣了
eg. java.lang.String
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
String底層是一個final的char陣列
private final char value[];
String的hashCode計算方式就是遍歷字元陣列 計算其加權和eg.Integer類
public int hashCode() {
return value;
}
hashCode()返回的是它值本身
java.lang.Object的約定:
我的理解是:equals方法是鑑定兩個物件邏輯相等的唯一標準
hashCode是物件的雜湊值 比如不同的ABCDEFGHI物件 它們經過一系列計算得到雜湊值(右邊),不同的物件是可能會有相同的雜湊值的
hashCode()就是一個提高效率的策略問題 ,因為hashCode是int型的,int型比較運算是相當快的。所以比較兩個物件是否相等,可以先比較其hashCode 如果hashCode不相等說明肯定是兩個不同的物件。但hashCode相等的情況下,並不一定equals也相等,就再執行equals方法真正來比較兩者是否相等
hashCode相等 無法表明equals相等,舉例:
比如:"a".hashCode() 結果是97;
new Integer(97).hashCode() 呢?也是97
但new Integer(97).equals("a") 結果是false
(計算方式上面已經闡述了,也可以用程式碼執行得出結果)
總結:
hashCode()方法存在的主要目的就是提高效率,把物件放到雜湊存儲結構的集合中時,先比hashCode()再比equals
注意:
重寫了equals方法則一定要重寫hashCode
就像String類一樣,equals方法不同於Object的equals,它的hashCode方法也不一樣。
為什麼:反證一下,如果不重寫hashCode,那麼兩個字串,不同的物件,但裡面的內容是一樣的,equals則返回true。但他們的hashCode卻返回例項的ID(記憶體地址)。那麼這兩個物件的hashCode就不相等了!
違反了上面所說的:equals方法是鑑定兩個物件邏輯相等的唯一標準
兩個物件的equals()方法等同===>兩物件hashCode()必相同。
hashCode()相同 不能推導 equals()相等
因此,重寫了equals方法則一定要重寫hashCode。保證上面的語句成立
4.HashMap
http://alex09.iteye.com/blog/539545
關於HashMap底層原始碼,這篇博文介紹的很清楚了
HashMap中維護了一個特別的資料結構,就是Entry
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
(Java裡面static一般用來修飾成員變數或函式。但有一種特殊用法是用static修飾內部類,普通類是不允許宣告為靜態的,只有內部類才可以。被static修飾的內部類可以直接作為一個普通類來使用,而不需例項一個外部類)
map.put("英語" , 78.2); -----------------> map.put(Key , Value);
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
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) {
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);
return null;
}
梳理總結一下:
put方法是:1)對Key的hashCode再運算得到hash值 (Key的hashCode)
2)計算hash值在table中的索引
3)索引處為空,則新增如此物件。
4)如果不為空:
a.遍歷索引處的連結串列,先比較hash值(注意,雖然它們在table的索引相等,不能說明hash相等) 再 真正比較equals 邏輯相等 ==物理相等 如果相等則覆蓋之前這個物件的value進行覆蓋
b.連結串列中沒有相等的,則將此物件加在這個索引的第一位(連結串列的頭插法)
插入節點的程式碼
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) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
總結:當程式試圖將一個 key-value 對放入 HashMap 中時,程式首先根據該 key 的 hashCode() 返回值決定該 Entry 的儲存位置:如果兩個 Entry 的 key 的 hashCode() 返回值相同,那它們的儲存位置相同。如果這兩個 Entry 的 key 通過 equals 比較返回 true,新新增 Entry 的 value 將覆蓋集合中原有 Entry 的 value,但 key 不會覆蓋。如果這兩個 Entry 的 key 通過 equals 比較返回 false,新新增的 Entry 將與集合中原有 Entry 形成 Entry 鏈,而且新新增的 Entry 位於 Entry 鏈的頭部
HashMap大概的樣子如下(個人意淫)
另外:
(1)負載因子
int threshold;
/**
* The load factor for the hash table.
*
* @serial
*/
final float loadFactor;
這個loadFactor就是我們的負載因子,它是怎麼用的?我找遍了原始碼沒找到相除然後和loadFactor比較的語句
原來是這樣的:
它藉助threshold,該變數包含了 HashMap 能容納的 key-value 對的極限,它的值等於 HashMap 的容量乘以負載因子(load factor)
在addEntry()方法中我們看到
新增之前先比較一下現在的size與threshold,如果超過則resize
如果沒有,則成功新增一個元素(key-value對)之後,size都會加1
(2)resize
將會建立原來HashMap大小的兩倍的bucket陣列,並將原來的物件放入新的bucket陣列。(因為新的陣列的length變化了,所以計算出來計算hash值在table中的索引也會有改變)。同時,table裡面每個坑裡面的連結串列也會倒置過來(自己體會)
重新調整HashMap大小存在什麼問題:多執行緒的情況下,可能產生條件競爭,它們會同時試著調整大小,死迴圈
======>不應該在多執行緒的情況下使用HashMap
Hashtable是synchronized的,但是ConcurrentHashMap同步效能更好
比較HashMap和Hashtable:
5.HashSet
HashSet的底層也是醉了 竟然是HashMap
private transient HashMap<E,Object> map;
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
/**
* Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
* default initial capacity (16) and load factor (0.75).
*/
public HashSet() {
map = new HashMap<>();
}
從源程式看,HashSet只是封裝了一個HashMap物件來儲存所有的集合元素。所有放入HashSet的值其實就相當於HashMap的Key,而Value是靜態final的Object,連註釋都要說它是Dummy value......
HashSet的大部分方法都是通過呼叫HashMap的方法來實現的
剩下的方法總結及HashSet底層還在研究中 未完待續...............
如果以上存在任何錯誤,歡迎指正