Hash和HashCode深入理解

目錄介紹

  • 1.Hash的作用介紹

    • 1.1 Hash的定義
    • 1.2 Hash函數特性
    • 1.3 Hash的使用場景
  • 2.如何判斷兩個對象相等

    • 2.1 判斷兩個字符串
    • 2.2 判斷兩個int數值
    • 2.3 其他基本類型
  • 3.HashCode深入分析

    • 3.0 HashCode是什麼
    • 3.1 爲什麼要重寫HashCode
    • 3.2 HashCode源代碼分析
    • 3.3 HashCode帶來的疑問
    • 3.4 HashCode的作用
    • 3.5 HashMap中的HashCode
    • 3.6 可直接用hashcode判斷兩個對象是否相等
  • 4.Hash表是什麼

    • 4.1 Hash表定義
    • 4.2 Hash表簡單介紹
  • 5.Hash中的算法應用

    • 5.1 基礎算法
    • 5.2 經典算法[摘自網絡]
    • 5.3 Hash碰撞[摘自網絡]
  • 6.Hash在Java中的應用場景

    • 6.1 equals與hashCode有兩個注意點
    • 6.2 以HashSet爲例說明hashCode()的作用
    • 6.3 以HashMap爲例說明Hash的作用
    • 6.4
  • 7.版本更新情況
  • 8.其他介紹

1.Hash的作用介紹

1.1 Hash的定義

  • 散列(哈希)函數

    • 把任意長度的輸入(又叫做預映射pre-image)通過散列算法變換成固定長度的輸出,該輸出就是散列值,是一種壓縮映射。
    • 或者說一種將任意長度的消息壓縮到某一固定長度的消息摘要的函數。

1.2 Hash函數特性

  • h(k1)≠h(k2)則k1≠k2,即散列值不相同,則輸入值即預映射不同

    • 如果k1≠k2,h(k1)=h(k2) 則發生碰撞;
    • 如果h(k1)=h(k2),k1不一定等於k2;

1.3 Hash的使用場景

  • 比如說我們下載一個文件,文件的下載過程中會經過很多網絡服務器、路由器的中轉,如何保證這個文件就是我們所需要的呢?我們不可能去一一檢測這個文件的每個字節,也不能簡單地利用文件名、文件大小這些極容易僞裝的信息,這時候,就需要一種指紋一樣的標誌來檢查文件的可靠性,這種指紋就是我們現在所用的Hash算法(也叫散列算法)。
  • 散列算法就是一種以較短的信息來保證文件唯一性的標誌,這種標誌與文件的每一個字節都相關,而且難以找到逆向規律。因此,當原有文件發生改變時,其標誌值也會發生改變,從而告訴文件使用者當前的文件已經不是你所需求的文件。
  • 這種標誌有何意義呢?之前文件下載過程就是一個很好的例子,事實上,現在大部分的網絡部署和版本控制工具都在使用散列算法來保證文件可靠性。

2.如何判斷兩個對象相等

2.1 判斷兩個字符串

  • 使用equals方法判斷兩個字符串是否相等
String a = "yc1";
String b = "yc2";
boolean isEqual = a.equals(b);
  • 當然Object的子類可以通過重寫equals的方法,實現子類自身的對象是否相等的邏輯;String是Object的子類,查看下它的equals方法
//在Object類中
public boolean equals(Object obj) {
    //直接比較的是地址
    return (this == obj);
}

//在String類中
public boolean equals(Object anObject) {
    //直接比較的是地址
    if (this == anObject) {
        return true;
    }
    //盤旋是否是字符串String類型
    if (anObject instanceof String) {
        String anotherString = (String) anObject;
        int n = count;
        if (n == anotherString.count) {
            int i = 0;
            //循環判斷每個字符是否相等
            while (n-- != 0) {
                if (charAt(i) != anotherString.charAt(i))
                        return false;
                i++;
            }
            return true;
        }
    }
    return false;
}

2.2 判斷兩個int數值

  • Integer類的equals方法又是如何實現的呢?
Integer a = Integer.valueOf("1");
Integer b = Integer.valueOf("2");
boolean ab = a.equals(b);


public boolean equals(Object obj) {
    //先判斷是否是Integer類型
    if (obj instanceof Integer) {
        //轉爲int值後進行比較
        return value == ((Integer)obj).intValue();
    }
    return false;
}

2.3 其他基本類型

//short類型
@Override
public int hashCode() {
    return Short.hashCode(value);
}
public static int hashCode(short value) {
    return (int)value;
}


//Byte類型
@Override
public int hashCode() {
    return Byte.hashCode(value);
}
public static int hashCode(byte value) {
    return (int)value;
}

//Long類型
@Override
public int hashCode() {
    return Long.hashCode(value);
}
public static int hashCode(long value) {
    return (int)(value ^ (value >>> 32));
}
//long類型作爲索引範圍太大,需要轉爲int類型。這裏簡單的獲取低32位容易導致散列不均,因爲高位部分沒有被利用。所以這裏採用邏輯右移32位,讓高32位和低32位進行XOR操作,導致高位低位都能被利用到

//Boolean類型
@Override
public int hashCode() {
    return Boolean.hashCode(value);
}
public static int hashCode(boolean value) {
    return value ? 1231 : 1237;
}
//採用兩個質數作爲true或false的索引。這兩個質數足夠大,用來作爲索引時,出現碰撞的可能性低。

3.HashCode深入分析

3.0 HashCode是什麼

  • HashCode是Object的一個方法,hashCode方法返回一個hash code值,且這個方法是爲了更好的支持hash表,比如String,Set,HashTable、HashMap等;

3.1 爲什麼要重寫HashCode

  • 如果用 equal 去比較的話,如果存在1000個元素,你 new 一個新的元素出來,需要去調用1000次equal去逐個和他們比較是否是同一個對象,這樣會大大降低效率。hashcode實際上是返回對象的存儲地址,如果這個位置上沒有元素,就把元素直接存儲在上面,如果這個位置上已經存在元素,這個時候纔去調用equal方法與新元素進行比較,相同的話就不存了,散列到其他地址上

3.2 HashCode源代碼分析

  • 在Object中的HashCode源代碼
public int hashCode() {
    int lockWord = shadow$_monitor_;
    final int lockWordStateMask = 0xC0000000; // Top 2 bits.
    final int lockWordStateHash = 0x80000000; // Top 2 bits are value 2 (kStateHash).
    final int lockWordHashMask = 0x0FFFFFFF; // Low 28 bits.
    if ((lockWord & lockWordStateMask) == lockWordStateHash) {
        return lockWord & lockWordHashMask;
    }
    //返回的是對象引用地址
    return System.identityHashCode(this);
}
  • 在String中的HashCode源代碼
public int hashCode() {
    int h = hash;
    if (h == 0 && count > 0) {
        for (int i = 0; i < count; i++) {
            h = 31 * h + charAt(i);
        }
        hash = h;
    }
    return h;
}
  • 在Integer中的HashCode源代碼
public int hashCode() {
    //int值
    return value;
}

3.3 HashCode帶來的疑問

  • 爲何重寫equals建議同時重寫hashCode?
  • hashCode是什麼?
  • hashCode作用?
  • hash code(hash值)是什麼?
  • hash table(hash表)是什麼?
  • hashCode方法對hash表有益處?
  • hashCode方法對不是hash有益處嗎?

3.4 HashCode的作用

  • 減少查找次數,提高程序效率

    • 例如查找是否存在重複值

      • h(k1)≠h(k2)則k1≠k2
      • 首先查看h(k2)輸出值(內存地址),查看該內存地址是否存在值;
      • 如果無,則表示該值不存在重複值;
      • 如果有,則進行值比較,相同則表示該值已經存在散列列表中,如果不相同則再進行一個一個值比較;而無需一開始就一個一個值的比較,減少了查找次數

3.5 HashMap中的HashCode

  • 在Java中也一樣,hashCode方法的主要作用是爲了配合基於散列的集合一起正常運行,這樣的散列集合包括HashSet、HashMap以及HashTable。
  • 爲什麼這麼說呢?考慮一種情況,當向集合中插入對象時,如何判別在集合中是否已經存在該對象了?(注意:集合中不允許重複的元素存在)

    • 也許大多數人都會想到調用equals方法來逐個進行比較,這個方法確實可行。但是如果集合中已經存在一萬條數據或者更多的數據,如果採用equals方法去逐一比較,效率必然是一個問題。
    • 此時hashCode方法的作用就體現出來了,當集合要添加新的對象時,先調用這個對象的hashCode方法,得到對應的hashcode值,實際上在HashMap的具體實現中會用一個table保存已經存進去的對象的hashcode值,如果table中沒有該hashcode值,它就可以直接存進去,不用再進行任何比較了;如果存在該hashcode值, 就調用它的equals方法與新元素進行比較,相同的話就不存了,不相同就散列其它的地址,所以這裏存在一個衝突解決的問題,這樣一來實際調用equals方法的次數就大大降低了,說通俗一點:Java中的hashCode方法就是根據一定的規則將與對象相關的信息(比如對象的存儲地址,對象的字段等)映射成一個數值,這個數值稱作爲散列值。下面這段代碼是java.util.HashMap的中put方法的具體實現:
  • put方法是用來向HashMap中添加新的元素,從put方法的具體實現可知,會先調用hashCode方法得到該元素的hashCode值,然後查看table中是否存在該hashCode值,如果存在則調用equals方法重新確定是否存在該元素,如果存在,則更新value值,否則將新的元素添加到HashMap中。從這裏可以看出,hashCode方法的存在是爲了減少equals方法的調用次數,從而提高程序效率。
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

3.6 可直接用hashcode判斷兩個對象是否相等

  • 肯定是不可以的,因爲不同的對象可能會生成相同的hashcode值。雖然不能根據hashcode值判斷兩個對象是否相等,但是可以直接根據hashcode值判斷兩個對象不等,如果兩個對象的hashcode值不等,則必定是兩個不同的對象。如果要判斷兩個對象是否真正相等,必須通過equals方法。
  • 也就是說對於兩個對象,如果調用equals方法得到的結果爲true,則兩個對象的hashcode值必定相等;

    • 如果equals方法得到的結果爲false,則兩個對象的hashcode值不一定不同;
    • 如果兩個對象的hashcode值不等,則equals方法得到的結果必定爲false;
    • 如果兩個對象的hashcode值相等,則equals方法得到的結果未知。

4.Hash表是什麼

4.1 Hash表定義

  • 根據關鍵碼值(KEY-VALUE)而直接進行訪問的數據結構;它通過把關鍵碼值(KEY-VALUE)映射到表中一個位置來訪問記錄,以加快查找的速度。這個映射函數叫做散列函數,存放記錄的數組叫做散列表。

4.2 Hash表簡單介紹

  • 將k作爲輸入值,h(k)輸出值作爲內存地址,該內存地址用來存放value,然後可以通過k獲取到value存放的地址,從而獲取value信息。

5.Hash中的算法應用

5.1 基礎算法

  • 比如,Java中的String.hashCode使用乘法和加法
public int hashCode() {
    int h = hash;
    if (h == 0 && count > 0) {
        for (int i = 0; i < count; i++) {
            //乘法與加法
            h = 31 * h + charAt(i);
        }
        hash = h;
    }
    return h;
}

5.2 經典算法[摘自網絡]

  • MD4,MD5,SHA-1或SHA-2等其他

5.3 Hash碰撞[摘自網絡]

  • hash是存在碰撞的,如果k1≠k2,h(k1)=h(k2) 則發生碰撞;

6.Hash在Java中的應用場景

6.1 equals與hashCode有兩個注意點

  • equals相同,則hashCode相同;而hashCode相同,equals不一定相同

    • 如果equals相同,hashCode不相同,有可能會造成上述重複值等情況,這種情況是不允許的;
    • 而hasCode相同,但是equals不一定相同,有可能是因爲發生了碰撞而碰撞是有可能性發生的

6.2 以HashSet爲例說明hashCode()的作用

  • 假設,HashSet中已經有1000個元素。當插入第1001個元素時,需要怎麼處理?

    • 因爲HashSet是Set集合,它允許有重複元素。“將第1001個元素逐個的和前面1000個元素進行比較”?
    • 顯然,這個效率是相等低下的。散列表很好的解決了這個問題,它根據元素的散列碼計算出元素在散列表中的位置,然後將元素插入該位置即可。對於相同的元素,自然是隻保存了一個。
    • 由此可知,若兩個元素相等,它們的散列碼一定相等;但反過來確不一定。在散列表中,

      • 1、如果兩個對象相等,那麼它們的hashCode()值一定要相同;
      • 2、如果兩個對象hashCode()相等,它們並不一定相等。
      • 注意:這是在散列表中的情況。在非散列表中一定如此!

6.3 以HashMap爲例說明Hash的作用

  • 在HashMap中有許多地方用到了hash算法
//put方法
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

//remove方法
public V remove(Object key) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}

//get方法
public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

//上面這幾個方法都用到了這個方法
static final int hash(Object key) {
    int h;
    //計算hashCode,並無符號移動到低位
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

舉個例子: h = 363771819^(363771819 >>> 16)
0001 0101 1010 1110 1011 0111 1010 1011(363771819)
0000 0000 0000 0000 0001 0101 1010 1110(5550) XOR
--------------------------------------- =
0001 0101 1010 1110 1010 0010 0000 0101(363766277)
這樣做可以實現了高地位更加均勻地混到一起

參考博客

關於其他內容介紹

01.關於博客彙總鏈接

02.關於我的博客

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