天天看點

hashCode花式賣萌

聲明:這篇博文純屬是最近看源碼時閑着沒事瞎折騰(好奇心驅動),對實際的應用程式編碼我覺得可能沒有那麼大的幫助,各位親就當是代碼寫累了放松放松心情,視為偏門小故事看一看就可以了,别深究。

一、從Object和System談起

首先是Object類中的hashCode()方法:

public native int hashCode();      

native修飾的方法。但是根據文檔的描述,我們知道這個int類型的hashCode是根據對象的位址轉換而來的。

引文:

As much as is reasonably practical, the hashCode method defined by class 

Object

 does return distinct integers for distinct objects. (This is typically implemented by converting the internal address of the object into an integer, but this implementation technique is not required by the Java™ programming language.)。

然後是System類的靜态方法:identityHashCode(Object x)

public static native int identityHashCode(Object x);      

不出所料,也是native方法。這個方法傳回的結果也是根據對象的位址轉換而來的,與Object的執行個體調用hashCode()傳回的結果一樣(前提是hashCode()方法未被子類重寫)。

但是還是有點小差别,就是在處理null值的時候:

System.identityHashCode(null)會傳回0,而指向null的Object對象調用hashCode()顯然會出現NPE異常。

package com.prac;
public class Obj {
    public static void main(String[] args) {
        Obj obj = null;
//        System.out.println(obj.hashCode());//NPE異常
        System.out.println(System.identityHashCode(null));//0
    }
}      

另外,由于數組木有重寫Object的hashCode()方法,是以任何數組對象調用hashCode()方法得到的都是基于對象位址計算出來的值,也即是與System.identityHashCode()方式調用傳回的結果一樣。

實驗一下:

int[] ary = new int[10];
        System.out.println(ary.hashCode());
        System.out.println(System.identityHashCode(ary));      

輸出:

778966024
778966024      

 以上根據對象位址來計算hashCode值是比較"原始簡單粗暴"的政策。一個對象本身的内容(屬性資料)改變後,其hashCode仍然不會變,例如下面這個例子。在很多實際應用場景中,這可能不是一個很好的政策。姑且将這種政策稱之為“base on address”,實際上這與後面要介紹的“base on content”形成一個比對效果。

ArrayList<Integer> list = new ArrayList<Integer>();
        list.add(1);
        System.out.println(System.identityHashCode(list));//1794515827
        list.add(2);
        System.out.println(System.identityHashCode(list));//1794515827,與改變前的hashCode一樣,因為對象位址沒變      

二、邂逅基本包裝類型

 2.1、Integer類型的hashCode實作

Integer當然也重寫了其間接父類Object的hashCode()方法,在JDK1.8以前,是直接傳回Integer對象的數值value,JDK1.8中增加了一個靜态方法,通過調用靜态方法也是直接傳回對象的數值value。

@Override
    public int hashCode() {
        return Integer.hashCode(value);
    }
    /**
     * @since 1.8
     */
    public static int hashCode(int value) {
        return value;
    }      

2.2、Long類型的hashCode實作

Long是将其值進行無符号右移32位的結果在與其本身做異或運算,最後傳回一個int值,嗯,好像有點意思!

@Override
    public int hashCode() {
        return Long.hashCode(value);
    }
    /**
     * @since 1.8
     */
    public static int hashCode(long value) {
        return (int)(value ^ (value >>> 32));
    }      

3.2、Character、Short和Byte

Character類型、Short類型和Byte類型基本是采用相同的方式,都是将char、short或者byte類型向上轉型為int類型,就直接傳回了,不一一列舉。

以Character為例:

@Override
    public int hashCode() {
        return Character.hashCode(value);
    }
    /**
     * @since 1.8
     */
    public static int hashCode(char value) {
        return (int)value;
    }      

3.3、Float和Double的hashCode實作

這兩個類型的實作大緻比較類似,但是稍微有點不好了解。

這裡以Float為例,Float類(>=JDK1.8)中求解hashCode過程中分别調用了如下的方法。

@Override
    public int hashCode() {
        return Float.hashCode(value);
    }
    /**
     * @since 1.8
     */
    public static int hashCode(float value) {
        return floatToIntBits(value);
    }
    public static int floatToIntBits(float value) {
        int result = floatToRawIntBits(value);
        // Check for NaN based on values of bit fields, maximum
        // exponent and nonzero significand.
        if ( ((result & FloatConsts.EXP_BIT_MASK) ==
              FloatConsts.EXP_BIT_MASK) &&
             (result & FloatConsts.SIGNIF_BIT_MASK) != 0)
            result = 0x7fc00000;
        return result;
    }
    public static native int floatToRawIntBits(float value);      

可見最終是調用floatToRawIntBits()方法,有點意思!

這個floatToRawIntBits()方法的官方注解是:

Returns a representation of the specified floating-point value according to the IEEE 754 floating-point "single format" bit layout, preserving Not-a-Number (NaN) values.

我個人了解就是将該浮點數在IEEE 754标準下的二進制位表示直接當成int類型的二進制位來解釋,也就是忽略掉IEEE 754标準的約定規則。API的方法命名floatToIntBits也大概是想表達這個語義吧。

IEEE 754标準表示32位浮點數格式分三部分:數符S(第31位),階碼E(第23位-30位共8位)和尾數M(剩下的23位)。

具體的求解過程舉個栗子:

還是以經典的浮點數Float valuef = 100.25f為例,IEEE 754标準表示過程如下:

hashCode花式賣萌

32位的二進制:0100 0010 1100 1000 1000 0000 0000 0000本來是根據IEEE 754标準表示的結果,但是計算hashCode時直接将這個二進制數按照int類型來解釋就是1120436224。

是以valuef 的hashCode應該是1120436224。

Float valuef = 100.25f;
        System.out.println("hashCode = "+valuef.hashCode());
        System.out.println("floatToIntBits = "+Float.floatToIntBits(valuef));
        System.out.println("floatToRawIntBits = "+Float.floatToRawIntBits(valuef));
        System.out.println("0x42C88000->decimal = "+Integer.parseInt("0x42C88000".substring(2),16));
        System.out.println(Float.toHexString(new Float(100.25f)));      
hashCode = 1120436224
floatToIntBits = 1120436224
floatToRawIntBits = 1120436224
0x42C88000->decimal = 1120436224
0x1.91p6      

Double類求hashCode與Float原理是類似的。

3.4、Boolean類型的hashCode實作

Boolean類型的hashCode最搞笑了,根據Boolean對象的值進行二者其選一:true->1231,false->1237。為什麼程式的實作者會選擇這兩個值作為其hashCode呢,有什麼故事嗎?不造也!或者就是作者個人癖好?

@Override
    public int hashCode() {
        return Boolean.hashCode(value);
    }
    /**
     * @since 1.8
     */
    public static int hashCode(boolean value) {
        return value ? 1231 : 1237;
    }      

三、拐角遇見String類

String類重寫了父類Object的hashCode()方法:

/**
    *===================================================================
    * String當然是重寫了Object的hashCode()方法
    * hashCode的生成邏輯:s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
    * 空字元串的hashCode是0
    *==================================================================
    */
    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;
    }      

 如果從數學角度來直覺的展現這個求解過程,大概是下面這樣的:

hashCode花式賣萌

化簡一下求解過程也很簡單:将前面n-1個等式的左右兩邊依次乘上31^(n-1)、31^(n-1)...31^2、31,再左右累加就可以錯位相消。n次疊代後,hashCode計算由如下公式的給出:

hashCode花式賣萌

當然這裡的hashCode很有可能因為大于-1>>>1而溢出傳回一個負數。

素數31在後面的其他hashCode方法中都很常見,似乎是一個所謂的“梅森素數”,沒研究過,選31也可能是考慮用移位運算(1<<5)-1比較快吧。

 四、眼看他起朱樓

有了以上的String和8種基本包裝類型的hashCode實作,其他類的hashCode基本上就是在其前面的基礎上做擴充,做疊代。

比如:工具類Arrays中,求一個long類型的數組的hashCode,就是将每個元素值的hashCode按照規則進行n次疊代,其源碼如下:

public static int hashCode(long a[]) {
        if (a == null)
            return 0;

        int result = 1;
        for (long element : a) {
            int elementHash = (int)(element ^ (element >>> 32));
            result = 31 * result + elementHash;
        }
        return result;
    }      

Arrays類針對不同的參數(byte[],int[],long[]等等)重載了很多hashCode()方法,基本都是按照此思路來疊代求解hashCode值的。 

這些疊代可以有一個通用公式:

hashCode花式賣萌

實際上,“将每個元素值的hashCode按照規則進行n次疊代”這樣的實作有一定的考究。Arrays中的這些方法都遵循一個所謂的“based on the contents”的原則,即這個傳回的hashCdoe隻與數組中的元素本身有關,跟這個數組對象的位址無關。

舉個栗子很容易驗證:

int[] aryA = {1,2,3};
        int[] aryB = {1,2,3};
        System.out.println("AryA ?= AryB : "+(aryA == aryB));
        System.out.println(Arrays.hashCode(aryA));//30817
        System.out.println(Arrays.hashCode(aryB));//30817,aryA和aryB顯然是兩個不同的對象,但是它們的"内容"相同      
AryA ?= AryB : false
30817
30817      

另外,針對對象數組,Arrays中提供兩個方法來求解其hashCode:hashCode(Object a[])和deepHashCode(Object a[])

 第一個方法可以了解為隻做到了一層“基于内容”(based on the contents)的調用,當對象數組中的元素還是一個數組時(也即多元數組),第二層實際上是根據數組對象位址來生成hashCode,這多少有點違背了“based on the contents”原則。這個層面來講,這是種“淺疊代”的方式。

想要嚴格的實作“基于内容”,可以通過第二個方法deepHashCode(Object a[]),該方法在每一層疊代時會對每一個元素進行類型校驗,以確定該元素都是基本類型的數組,如果不是,就繼續遞歸下去。這樣真正做到了“based on the contents”。

public static int hashCode(Object a[]) {
        if (a == null)
            return 0;

        int result = 1;

        for (Object element : a)
            result = 31 * result + (element == null ? 0 : element.hashCode());

        return result;
    }

    public static int deepHashCode(Object a[]) {
        if (a == null)
            return 0;

        int result = 1;

        for (Object element : a) {
            int elementHash = 0;
            if (element instanceof Object[])
                elementHash = deepHashCode((Object[]) element);
            else if (element instanceof byte[])
                elementHash = hashCode((byte[]) element);
            else if (element instanceof short[])
                elementHash = hashCode((short[]) element);
            else if (element instanceof int[])
                elementHash = hashCode((int[]) element);
            else if (element instanceof long[])
                elementHash = hashCode((long[]) element);
            else if (element instanceof char[])
                elementHash = hashCode((char[]) element);
            else if (element instanceof float[])
                elementHash = hashCode((float[]) element);
            else if (element instanceof double[])
                elementHash = hashCode((double[]) element);
            else if (element instanceof boolean[])
                elementHash = hashCode((boolean[]) element);
            else if (element != null)
                elementHash = element.hashCode();

            result = 31 * result + elementHash;
        }

        return result;
    }      

其他的像Objects類(Object的工具類,也是null值相容的),以及集合對象(在它們的抽象父類:AbstractList,AbstractMap,AbstractSet等中實作了hashCode()方法)大緻都是按照Arrays中的實作思路來實作的,而且也都遵循"based on the contents"這一基本原則。

int[] aryA = {1,2,3};
        ArrayList<Integer> list = new ArrayList<Integer>();
        list.add(1);
        list.add(2);
        list.add(3);
        System.out.println(Arrays.hashCode(aryA));//30817
        System.out.println(list.hashCode());//30817      

 五、結局了,散了吧

大結局了,還是簡單看看一下HashMap中的hashCode實作,本來HashMap沒有什麼特殊性的。

準确的說這裡說的是HashMap中的節點元素Node的hashCode實作。

因為HashMap内部實際上是用了一個Node<K,V>類型的數組來存儲HashMap對象的有效資料,Node對象中分别有四個屬性:hash,kye,value以及指向下一個Node對象的next。大緻可以用如下圖這樣來了解:

hashCode花式賣萌

其内部類Node的hashCode()實作:

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
        
        //其他如構造器,getter方法略掉
        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }
        //其他方法略掉
    }      

這個hashCode實作有點欲言又止,在源碼中似乎沒有看到這個Node的hashCode()方法的調用?

在JDK1.8中的HashMap對象調用put方法中hash值是采用如下方式生成:

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }      

是以HashMap的hash值也是站在前面的肩膀上的。

全劇終!

從此hashCode和程式員過上了幸福美滿的生活!