HashMap深入解析

  1. HashMap原理

Hashmap是数组和链表的结合体,数组每个元素存的是链表的头结点往hashmap里面存放键值对的时候先得到key的hashcode,然后重新计算hashcode,(让1均匀分布,因为如果不均匀分布,低位全是0,则后来计算数组下标的时候会冲突),然后与length-1按位与,计算数组下标,如果该下标对应的链表为空,则直接把键值对作为链表头结点,如果不为空,则遍历链表看是否有key值相同的,有就把value换掉,没有就把该对象作为链表的第一个结点,原有的结点作为他的后续结点。

  1. HashCode参数及其扩容机制

初始容量为16.达到阀值扩容,阀值等于最大扩容*负载因子,扩容每次2倍,总是2的n次方。

扩容机制:

使用一个容量更大的数组来代替已有容量小的数组,transfer()方法将原来的Entry数组的元素拷贝到新的Entry中,java1.7中重新计算每个元素在数组里的位置,java1.8中不是重新计算,而是用了更为巧妙的方法。

 

 

  1. HashMap的Get方法源码

HashMap取值方法是通过哈希方法来计算key值所在位置进行获取。key值可为null。方法如下:

  /**

     * 获取key匹配的值

     * 如果map中有匹配到的key,则返回key对应的值;

     * 如果没有匹配到,则返回null

     *

     * 如果返回null可能是未匹配到值,  

     * 可以用containsKey来区分

     *

     * @see #put(Object, Object)

     */

    public V get(Object key) {

        //key为null时获取方式

        if (key == null)

            return getForNullKey();

        //根据key计算哈希值

        int hash = hash(key.hashCode());

        //根据哈希值和数组长度查找数组的位置的链表表头

        for (Entry<K,V> e = table[indexFor(hash, table.length)];

             e != null;

             e = e.next) {

            Object k;

            //匹配key值

            if (e.hash == hash && ((k = e.key) == key || key.equals(k)))

                //匹配到,则返回value

                return e.value;

        }

        //匹配不到,则返回null

        return null;

    }

 

当key为null时,从数组下标为0的位置取,获取方法为:

 private V getForNullKey() {

        for (Entry<K,V> e = table[0]; e != null; e = e.next) {

            if (e.key == null)

                return e.value;

        }

        return null;

    }

get方法获取值时,当key为null时,从数组下标为0的位置取。当key不为null时,则跟hash方法和indexFor方法计算位置来获取值。

 

 

  1. HashMap的put的方法源码

put方法的源码如下:

  1.  


public V put(K key, V value) {

        //在JDK1.8中,实际上调用的putVal方法

        return putVal(hash(key), key, value, false, true);

    }

 

putVal方法:

 

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {

        Node<K,V>[] tab;

        Node<K,V> p;

        int n, i;

        //1.判断tab是否为空,是否还有剩余空间,是否需要扩容操作

        if ((tab = table) == null || (n = tab.length) == 0)

            n = (tab = resize()).length;

        //给p赋值,为一个节点的位置,若为空,创建一个新节点,调用的LinkedHashMap的newNode方法

        if ((p = tab[i = (n - 1) & hash]) == null)

            tab[i] = newNode(hash, key, value, null);

        else {

            Node<K,V> e;

            K k;

            //1.比较哈希值是否相同;2.比较key实例是否相同;3.比较key的值

            if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))

            //若值已经存在,直接覆盖

                e = p;

            //若p是一个TreeNode对象

            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);

                        //如果链表的长度大于8了,进行链表->红黑树的转换操作

                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st

                            treeifyBin(tab, hash);

                        break;

                    }

                    //如果存在KEY值,直接赋值覆盖value

                    if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))

                        break;

                    p = e;

                }

            }

            //上面过程为给e这个Node<K,V>对象赋值

            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;

    }

 

5.HashCode值的计算

参考链接:https://blog.csdn.net/qq_38182963/article/details/78940047

二进制计算的一些基础知识

<< : 左移运算符,num << 1,相当于num乘以2  低位补0
>> : 右移运算符,num >> 1,相当于num除以2  高位补0
>>> : 无符号右移,忽略符号位,空位都以0补齐
 % : 模运算 取余
^ :   位异或 第一个操作数的的第n位于第二个操作数的第n位相反,那么结果的第n为也为1,否则为0
 & : 与运算 第一个操作数的的第n位于第二个操作数的第n位如果都是1,那么结果的第n为也为1,否则为0
 | :  或运算 第一个操作数的的第n位于第二个操作数的第n位 只要有一个是1,那么结果的第n为也为1,否则为0
 ~ : 非运算 操作数的第n位为1,那么结果的第n位为0,反之,也就是取反运算(一元操作符:只操作一个数)

 

2.为什么使用 hashcode

那么我们就说说为什么使用 hashcode ,hashCode 存在的第一重要的原因就是在 HashMap(HashSet 其实就是HashMap) 中使用(其实Object 类的 hashCode 方法注释已经说明了 ),我知道,HashMap 之所以速度快,因为他使用的是散列表,根据 key 的 hashcode 值生成数组下标(通过内存地址直接查找,没有任何判断),时间复杂度完美情况下可以达到 n1(和数组相同,但是比数组用着爽多了,但是需要多出很多内存,相当于以空间换时间)。

 

3.HashMap 的 hash 算法的实现原理(为什么右移 16 位,为什么要使用 ^ 位异或)

乍看一下就是简单的异或运算和右移运算,但是为什么要异或呢?为什么要移位呢?而且移位16?

在分析这个问题之前,我们需要先看看另一个事情,什么呢?就是 HashMap 如何根据 hash 值找到数组的对象,我们看看 get 方法的代码:

我们看看代码中注释下方的一行代码:first = tab[(n - 1) & hash])。

使用数组长度减一 &运算 hash 值。这行代码就是为什么要让前面的 hash 方法移位并异或。

我们分析一下:

首先,假设有一种情况,对象 A 的 hashCode 为 1000 0100 0111 0001 0000 0111 1000 0000,对象 B 的 hashCode 为 0111 0111 0011 1000 1010 0001 0100 0000。

如果数组长度是16,也就是 15 与运算这两个数, 你会发现结果都是0。这样的散列结果太让人失望了。很明显不是一个好的散列算法。

但是如果我们将 hashCode 值右移 16 位,也就是取 int 类型的一半,刚好将该二进制数对半切开。并且使用位异或运算(如果两个数对应的位置值相反,则结果为1,反之为0),这样的话,就能避免我们上面的情况的发生。

总的来说,使用位移16位和异或就是防止这种极端情况。但是该方法在一些极端情况下还是有问题,比如:10000000000000000000000000 和 1000000000100000000000000 这两个数,如果数组长度是16,那么即使右移16位,在异或,hash 值还是会重复。但是为了性能,对这种极端情况,JDK 的作者选择了性能。毕竟这是少数情况,为了这种情况去增加 hash 时间,性价比不高。

4.HashMap 的容量为什么建议是 2的幂次方?

到这里,我们提了一个关键的问题: HashMap 的容量为什么建议是 2的幂次方?正好可以和上面的话题接上。楼主就是这么设计的。

为什么要 2 的幂次方呢?

我们说,hash 算法的目的是为了让hash值均匀的分布在桶中(数组),那么,如何做到呢?试想一下,如果不使用 2 的幂次方作为数组的长度会怎么样?

假设我们的数组长度是10,还是上面的公式:

1010 & 101010100101001001000 结果:1000 = 8

1010 & 101000101101001001001 结果:1000 = 8

1010 & 101010101101101001010 结果: 1010 = 10

1010 & 101100100111001101100 结果: 1000 = 8

看到结果我们惊呆了,这种散列结果,会导致这些不同的key值全部进入到相同的插槽中,形成链表,性能急剧下降。

所以说,我们一定要保证 & 中的二进制位全为 1,才能最大限度的利用 hash 值,并更好的散列,只有全是1 ,才能有更多的散列结果。如果是 1010,有的散列结果是永远都不会出现的,比如 0111,0101,1111,1110…,只要 & 之前的数有 0, 对应的 1 肯定就不会出现(因为只有都是1才会为1)。大大限制了散列的范围。

  1. 我们自定义 HashMap 容量最好是多少?

那我们如何自定义呢?自从有了阿里的规约插件,每次楼主都要初始化容量,如果我们预计我们的散列表中有2个数据,那么我就初始化容量为2嘛?

 

绝对不行,如果大家看过源码就会发现,如果Map中已有数据的容量达到了初始容量的 75%,那么散列表就会扩容,而扩容将会重新将所有的数据重新散列,性能损失严重,所以,我们可以必须要大于我们预计数据量的 1.34 倍,如果是2个数据的话,就需要初始化 2.68 个容量。当然这是开玩笑的,2.68 不可以,3 可不可以呢?肯定也是不可以的,我前面说了,如果不是2的幂次方,散列结果将会大大下降。导致出现大量链表。那么我可以将初始化容量设置为4。 当然了,如果你预计大概会插入 12 条数据的话,那么初始容量为16简直是完美,一点不浪费,而且也不会扩容。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值