HashMap 底层实现 ——未完待续

hashMap源码阅读 笔记记录以及Debug流程记录

hashmap源码大概看了一天半 其中有很多细节,本次抽出面试经常问的一些细节以及谈谈自己的理解 ,文章最后会贴出一些比较好的博客。 带有具体注释的源码会放在我的github上。这篇文章不会有太多源码的内容

数据结构

HashMap1.8中

  • 底层数据结构:使用的是数组+链表 +红黑树

HashMap1.7中

  • 底层数据结构:使用的是数组+链表

数组+链表 好处是 平衡数组添加删除时间复杂度高的影响以及减少链表查询时间复杂度高的办法

链表+红黑树 好处是:平衡空间和时间,红黑树查找效率优化可以达到O(logn)级别,但是每个节点需要存储左右指针,在hashmap中树节点还要存储next指针,以便在链表长度小于6的时候转变为链表结构。

头插法和尾插法

hashmap高并发环境下为什么会出现死锁

数组初始容量设置为16原因?

hashMap中数组的初始化容量设置为16,原因是为了在通过节点的hash值计算数组(也称哈希桶)下标的时候使得hash结点分布均匀。

通过一个例子来说明

在获取哈希桶下标的时使用哈希值对数组长度取模操作,保证下标在数组长度之中。源码中使用逻辑操作减少取模的时间开销。

first = tab[(n - 1) & hash]
复制代码

首先需要了解hash值为int型,为32位。假设我们求出hash为 1010100111 前面取0 那么来看看对于哈希桶为10以及16计算的不同结果。

十进制转二进制 10->1010B 16->10000B

1010B-1=1001B 10000B-1=1111B

 1010100111                1010100111   

 &     1001                      1111

       0001                      0111
复制代码

上面计算出来的下标是不同的没有问题,但是如果另外一个元素要put进hashMap中,他的hash码为1010100011

1010100011                1010100011   

&     1001                      1111

      0001                      0011
复制代码

我们可以直观看出两个hash最后四位的码不同却被放入相同的下标中,这样的缺点是,在特定下标中数组的链表长度会变得很长。搜索的效率变低,使得数组长度为16却可以保证每个链表中,每个节点的hash值是相同的。这是典型的利用空间换取时间思想。同理32,64 128也是如此。为此我们可以看到数组初始化大小上面的注释 。

    /**
     * The default initial capacity - MUST be a power of two.
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
复制代码

从初始化大小为16我们了解了hashmap数组容量大小的奥秘。

如果你想了解一下HashMap中是如何保证数组容量大小为2的N次方你可以看下tableSizeFor()函数

static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }
复制代码

java8中 hashmap 的优化 -> 扰动函数的优化

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}
复制代码

在hashMap中获取数组元素 底层会调用getNode()方法里面多添加了一个hash()函数进入这个hash()函数可以看到内部代码也很简单

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

这边调用key本身的hashCode()方法后,与他自身进行高十六位进行一个异或操作。这个操作的目的使得hashCode码不仅仅与低十六位有关,而且有高十六位也有关系,增加了hash的复杂性避免低效hashcode函数导致哈希字的碰撞仅在低位有关,使得计算出来的hash码更加均匀。

与JDK1.7中进行比较 发现两者的思想相同,但是实现方式不同。

h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
复制代码

HashMap中解决碰撞的方法

简单的说往数组中put值,数组为空就直接放入,有元素则放入元素后面。具体是链表的后面还是红黑树的后面,不是本篇的重点。

这边需要注意的是只有当数组的大小超过64并且单链表长度>=8的时候才会调用函数将链表转换为红黑树。

String类中hashcode()函数计算为什么选择31作为乘数因子?

 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;
复制代码

先是使用key自带的hashcode()方法然后 添加扰动函数 h>>>16 位然后进行一个操作

String 类的hashcode方法使用31作为优质乘数因子的两个原因

  • 31*i =(2>>5)*i -i jvm可以对他进行一定的优化
  • 使用31进行hash运算的时候 不会出现2质数的hash冲突率过高的以及101 质数hash值计算溢出的问题。相对与其他质数17 29等来说,利用hash进行hashcode计算的时候,分布更加均与。

基于以上两个原因选择31作为String类hashcode的计算结果。

以下的内容以及带注释的源码会在这两天进行一个补充

equals()和hashCode()的应用

以及它们在HashMap中的重要性

不可变对象的好处

HashMap多线程的条件竞争

重新调整HashMap的大小 rehash的使用以及rehash中判断下标

猜你喜欢

转载自juejin.im/post/5e61047e6fb9a07ca24f5c2a
今日推荐