1. 程式人生 > >java的HashCode equals == 以及hashMap底層實現深入理解

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底層還在研究中  未完待續...............

如果以上存在任何錯誤,歡迎指正