Java高级之1.7版本JDK中的HashMap的hash(key)方法

在说hash(key)方法之前,下面来简单模拟下HashMap中的put()方法,来查看哈希冲突:

最简单的情况,在下面代码中,用一个Object[]数组充当链表数组,判断str实例对象的hashCode值并得到在数组中存放的下标,然后放入数组。

/*
    说明:
        1.有一些对象需要放入到一个数组中(为什么要把它们放入数组,可以探究为什么会产生哈希算法?)
        2.问题来了,怎么确定该对象应该放到数组中哪个位置,不可能每个程序员都制定一套规则,让这个对象放到某个位置,没有统一的标准
        3.哈希算法就可以将一个对象映射成一串二进制,这串二进制又可以转换成十进制整数
        4.也就是某对象和它通过哈希算法产生的十进制整数是映射关系,对应的
        5.可以通过十进制来研究该把放入到数组中哪个位置
        6.发现用十进制数对数组长度取模,可以将所有对象都限制在数组长度范围内
        7.这就解决了把这对象放到数组中哪个位置的问题
 */
public class Demo {
    public static void main(String[] args) {
        String str=new String("abc");
        System.out.println(str);
        // 获取str的hashCode值
        int hashCode = str.hashCode();
        System.out.println(hashCode);
        // 将获得的所有对象存放到一个长度为20的数组中
        Object[] arr=new Object[20];
        int index = hashCode%arr.length;
        System.out.println(index);
        // 然后将该对象放入数组中对应下标的位置
        arr[index]=str;
        System.out.println(Arrays.toString(arr));
    }
}

当加入很多个对象时,可能某个对象的hashCode值对数组长度取模得到的下标,在数组中已经存在某个对象了,这就产生了冲突

/*
    说明:
        1.有一些对象需要放入到一个数组中(为什么要把它们放入数组,可以探究为什么会产生哈希算法?)
        2.问题来了,怎么确定该对象应该放到数组中哪个位置,不可能每个程序员都制定一套规则,让这个对象放到某个位置,没有统一的标准
        3.哈希算法就可以将一个对象映射成一串二进制,这串二进制又可以转换成十进制整数
        4.也就是某对象和它通过哈希算法产生的十进制整数是映射关系,对应的
        5.可以通过十进制来研究该把放入到数组中哪个位置
        6.发现用十进制数对数组长度取模,可以将所有对象都限制在数组长度范围内
        7.这就解决了把这对象放到数组中哪个位置的问题
        8.但又要添加一个对象到数组中,也通过hashCode值确定了该放在数组中哪个位置
        9.但是,在该位置已经存在一个对象了,而对于这种多个对象共用一个地址的情形,就称之为冲突
        10.有了冲突,自然是需要解决冲突,解决冲突的其中一种就是链地址法,也称为拉链法
 */
public class Demo {
    public static void main(String[] args) {
        String str=new String("abc");
        System.out.println(str);
        // 获取str的hashCode值
        int hashCode = str.hashCode();
        System.out.println(hashCode);
        // 将获得的所有对象存放到一个长度为20的数组中
        Object[] arr=new Object[20];
        int index = hashCode%arr.length;
        System.out.println(index);
        // 然后将该对象放入数组中对应下标的位置
        arr[index]=str;
        System.out.println(Arrays.toString(arr));

        System.out.println("---------------------------------------------");

        // 现在又要往数组中添加一个对象
        String str2=new String("r");
        System.out.println(str2);
        int hashCode2 = str2.hashCode();
        System.out.println(hashCode2);
        int index2=hashCode2%20;
        System.out.println(index2);
        arr[index]=str2;// 发现"abc"和"r"在数组中存储的地址是一样的,这产生了冲突
        System.out.println(Arrays.toString(arr));
    }
}

事实上,我们并不希望后面的对象将已经存在的覆盖掉,就必须解决冲突,通过将元素存储在LinkedList中来模拟存储冲突元素的链表。

public class Demo2 {
    public static void main(String[] args) {
        String str=new String("abc");
        System.out.println(str);
        // 获取str的hashCode值
        int hashCode = str.hashCode();
        System.out.println(hashCode);
        // 将获得的所有对象存放到一个长度为20的数组中
        Object[] arr=new Object[20];
        int index = hashCode%arr.length;
        System.out.println(index);
        // 然后将该对象放入数组中对应下标的位置
        LinkedList<String> linkedList=new LinkedList<>();
        linkedList.add(str);
        arr[index]=linkedList;
//        arr[index]=str;
        System.out.println(Arrays.toString(arr));

        System.out.println("---------------------------------------------");

        // 现在又要往数组中添加一个对象
        String str2=new String("r");
        System.out.println(str2);
        int hashCode2 = str2.hashCode();
        System.out.println(hashCode2);
        int index2=hashCode2%20;
        System.out.println(index2);
        if(arr[index2]!=null){// 不为null的话表示已经存在元素了,采用链地址法,将地址相同的对象挂在后面
            LinkedList<String> list = (LinkedList)arr[index2];
            list.add(str2);
        }
//        arr[index]=str2;// 发现"abc"和"r"在数组中存储的地址是一样的,这产生了冲突
        System.out.println(Arrays.toString(arr));
    }
}

下面是来模拟26个英文字母存放到哈希表中:

public class Demo3 {
    public static void main(String[] args) {
        // 26个英文字母的字符串
        String va = "abcdefghijklmnopqrstuvwxyz";
        // 将字符串转成字符数组
        char[] vaArr = va.toCharArray();
        // 创建一个链表数组,用来存储对象
        Object[] obj = new Object[10];
        // 循环遍历字符数组
        for (Character c : vaArr) {
            // 获取到该字符对象的hashCode值,是一个十进制的整数
            int hashCode = c.hashCode();
            // 将hashCode值对数组长度取模,得到该字符对象应该存放到数组的哪一个位置
            int index = hashCode % obj.length;
            // 判断得到的下标在数组中是否已经有元素,没有元素则为null
            if (obj[index] == null) {
                // 创建一个链表,存放冲突的元素
                LinkedList<Object> linkedList = new LinkedList<>();
                // 添加元素到链表中
                linkedList.add(c);
                // 将指定下标处赋值
                obj[index] = linkedList;
            } else {
                // 如果已经存在元素了,就发生了冲突,解决冲突的办法就是将结点挂在已有的元素后面,这就是拉链法解决
                // 获取已经存在的链表
                LinkedList<Object> linkedList = (LinkedList<Object>) obj[index];
                // 将元素添加进去
                linkedList.add(c);
            }
        }
        // 打印数组
        System.out.println(Arrays.toString(obj));
    }
}

看到打印结果真的是冲突的元素非常多,将数组长度调整为20,情况有所好转

调为50,发现冲突的元素就少了很多,几乎没有冲突。

上面这些只是对哈希表有个更加深刻的认识,下面来谈谈hash(key)方法。

通过上面的代码就知道了哈希表的原理很简单,调用Object对象的hashCode()方法,该方法会返回一个整数,然后用这个数对HashMap或者HashTable的容量进行取模就行了。

但HashMap的实现却不是这样简单,更多的需要为效率考虑。

所以hash(key)方法就是返回哈希值,查看源码如下:

    final int hash(Object k) {
        int h = hashSeed;
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }

        h ^= k.hashCode();

        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

那么思考下,为什么不直接如下面这样呢?

int hash (Object k) {
    return k.hashCode();
}

多么简洁易读,而且我们上面的模拟就是这样干的,那为什么不这样呢?

看下面的代码:

public class Demo2 {
    public static void main(String[] args) {
        String str=new String("abc");
        System.out.println(str);
        // 获取str的hashCode值
        int hashCode = str.hashCode();
        System.out.println(hashCode);
        // 将获得的所有对象存放到一个长度为16的数组中,满足使用位运算的情况,length是2的n次方
        Object[] arr=new Object[16];

        int index = hashCode%arr.length;
        System.out.println(index);
        // 然后将该对象放入数组中对应下标的位置
        LinkedList<String> linkedList=new LinkedList<>();
        linkedList.add(str);
        arr[index]=linkedList;
        System.out.println(Arrays.toString(arr));

        System.out.println("---------------------------------------------");

        // 现在又要往数组中添加一个对象
        String str2=new String("r");
        System.out.println(str2);
        int hashCode2 = str2.hashCode();
        System.out.println(hashCode2);
        int index2=hashCode2%arr.length;
        System.out.println(index2);
        if(arr[index2]!=null){// 不为null的话表示已经存在元素了,采用链地址法,将地址相同的对象挂在后面
            LinkedList<String> list = (LinkedList)arr[index2];
            list.add(str2);
        }else {
            LinkedList<String> list=new LinkedList<>();
            list.add(str2);
            arr[index2]=list;
        }
        System.out.println(Arrays.toString(arr));
    }
}

发现index最后得出都是2,发生了冲突

使用了扰动算法,代码如下:

public class Demo2 {
    public static void main(String[] args) {
        String str = new String("abc");
        System.out.println(str);
        // 获取str的hashCode值
        int hashCode = str.hashCode();
        System.out.println(hashCode);
        // 将获得的所有对象存放到一个长度为16的数组中,满足使用位运算的情况,length是2的n次方
        Object[] arr = new Object[16];

        int index = hash(str) % arr.length;
        System.out.println(index);
        // 然后将该对象放入数组中对应下标的位置
        LinkedList<String> linkedList = new LinkedList<>();
        linkedList.add(str);
        arr[index] = linkedList;
        System.out.println(Arrays.toString(arr));

        System.out.println("---------------------------------------------");

        // 现在又要往数组中添加一个对象
        String str2 = new String("r");
        System.out.println(str2);
        int hashCode2 = str2.hashCode();
        System.out.println(hashCode2);
        int index2 = hash(str2) % arr.length;
        System.out.println(index2);
        if (arr[index2] != null) {// 不为null的话表示已经存在元素了,采用链地址法,将地址相同的对象挂在后面
            LinkedList<String> list = (LinkedList) arr[index2];
            list.add(str2);
        } else {
            LinkedList<String> list = new LinkedList<>();
            list.add(str2);
            arr[index2] = list;
        }
        System.out.println(Arrays.toString(arr));
    }

    /**
     * 返回使用了扰动算法的hashCode
     *
     * @param k 要计算hashCode值的对象
     * @return 返回使用了扰动算法的hashCode
     */
    static int hash(Object k) {
        int h = 0;
        h ^= k.hashCode();

        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }
}

其中hash()方法内的代码就是JDK中hash()方法的源码,对hashCode值进行扰动计算。

发现原来冲突的两个对象,现在不冲突了,下标index变成了2和5。

下面来看看扰动算法的每一步结果:

  "abc" "r"
k.hashCode()得到的十进制值 96354 114
k.hashCode()转换的二进制值 0000 0000 0000 0001 0111 1000 0110 0010 0000 0000 0000 0000 0000 0000 0111 0010
h=0 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
h^=k.hashCode() 0000 0000 0000 0001 0111 1000 0110 0010 0000 0000 0000 0000 0000 0000 0111 0010
h>>>20 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
h>>>12 0000 0000 0000 0000 0000 0000 0001 0111 0000 0000 0000 0000 0000 0000 0000 0000
h ^= (h >>> 20) ^ (h >>> 12) 0000 0000 0000 0001 0111 1000 0111 0101 0000 0000 0000 0000 0000 0000 0111 0010
h >>> 7 0000 0000 0000 0000 0000 0010 1111 0000 0000 0000 0000 0000 0000 0000 0000 0000
h >>> 4 0000 0000 0000 0000 0001 0111 1000 0111 0000 0000 0000 0000 0000 0000 0000 0111
return h ^ (h >>> 7) ^ (h >>> 4) 0000 0000 0000 0001 0110 1101 0000 0010 0000 0000 0000 0000 0000 0000 0111 0101
h ^ (h >>> 7) ^ (h >>> 4) % 16 0000 0000 0000 0000 0000 0000 0000 0010 0000 0000 0000 0000 0000 0000 0000 0101
转换成十进制 2 5

所以hash(key)扰动函数是对key的hashCode进行扰动运算,让高位也参与运算,让分布更加散列,最终达到降低哈希冲突的目的。

注意:这是JDK1.7中的hash方法,而1.8变化较大。

总结:

  • hash(key)方法是为了获取到键(key)的hashCode,并使用扰动算法,得到更加散列的值,降低发生哈希冲突的概率
  • indexFor(hash, length)方法是为了获取该对象应该存储在链表数组中哪个下标位置,使用位运算(&)能够提高效率

参考博客:

全网把Map中的hash()分析的最透彻的文章,别无二家。

Java高级之1.7版本JDK中的HashMap的indexFor()方法

猜你喜欢

转载自blog.csdn.net/cnds123321/article/details/113745628
今日推荐