HashMap原理

散列表

定义:
通过散列函数把元素的键值映射为下标,然后将数据存储在数组中对应下标的位置。当我们按照键值查询元素时,我们用同样的散列函数,将键值转化数组下标,从对应的数组下标的位置取数据。

装载因子:
散列表的装载因子=填入表中的元素个数/散列表的长度。
装载因子越大,说明空闲位置越少,冲突越多,散列表的性能会下降。

散列冲突解决方案

  1. 开放寻址法
    线性探测
    插入过程:
    如果某个数据经过散列函数散列之后,存储位置已经被占用了,我们就从当前位置开始,依次往后查找,看是否有空闲位置,直到找到为止。
    查找过程:
    我们通过散列函数求出要查找元素的键值对应的散列值,然后比较数组中下标为散列值的元素和要查找的元素。如果相等,则说明就是我们要找的元素;否则就顺序往后依次查找。如果遍历到数组中的空闲位置,还没有找到,就说明要查找的元素并没有在散列表中。
    删除过程:
    将删除的元素,特殊标记为deleted。当线性探测查找的时候,遇到标记为deleted的空间,并不是停下来,而是继续往下探测。
    适用条件:
    当数据量比较小、装载因子小的时候,适合采用开放寻址法。
    ThreadLocalMap使用开放寻址法解决散列冲突。

  2. 链表法
    在散列表中,每个桶会对应一条链表,所有散列值相同的元素都会放到相同槽位对应的链表中。
    内存的利用率比开放寻址法要高,对大装载因子的容忍度更高。
    比较适合存储大对象、大数据量的散列表,而且,比起开放寻址法,它更加灵活,支持更多的优化策略,比如用红黑树代替链表。

如何设计散列表

  1. 散列函数
    散列函数的设计不能太复杂。会消耗很多计算时间,也就间接地影响到散列表的性能。
    散列函数生成的值要尽可能随机并且均匀分布,这样才能避免或者最小化散列冲突,而且即便出现冲突,散列到每个槽里的数据也会比较平均,不会出现某个槽内数据特别多的情况。

  2. 解决负载因子过大问题
    扩容,数组长度扩大为之前2倍,负载因子原先是0.8也会减到0.4。
    散列表的大小变了,数据的存储位置也变了,所以需要通过散列函数重新计算每个数据的存储位置。

  3. 如何解决低效扩容
    为了解决一次性扩容耗时过多的情况,我们可以将扩容操作穿插在插入操作的过程中,分批完成。当装载因子触达阈值之后,我们只申请新空间,但并不将老的数据搬移到新散列表中。
    当有新数据要插入时,我们将新数据插入新散列表中,并且从老的散列表中拿出一个数据放入到新散列表。这样没有了集中的一次性数据搬移,插入操作就都变得很快了。
    查询先从新散列表中查找,再从旧散列表中查找。

HashMap

HashMap是一个利用哈希表原理来存储元素的集合。遇到冲突时,HashMap是采用的链表法来解决。
在JDK1.7中,HashMap是由数组+链表构成的。在JDK1.8中,HashMap是由数组+链表+红黑树构成。

HashMap初始化容量16,负载因子0.75。
链表长度大于8会转成红黑树,小于6,红黑树又会转成链表。
当极端情况,所有数据都装进一个桶内,使用查询时间是O(n),使用红黑树查询时间是O(logn),在数据量较大且哈希碰撞较多时,能够极大的增加检索的效率。在数据量较小的情况下,红黑树要维护平衡,比起链表来,性能上的优势并不明显,空间成本比较高。
允许key和value都为null。key重复会被覆盖,value允许重复。
无序。

字段属性

private static final long serialVersionUID = 362498820763181265L;
// 集合初始化容量
static final int DEFAULT_INITIAL_CAPACITY = 16;
// 集合的最大容量
static final int MAXIMUM_CAPACITY = 1073741824;
// 默认的填充因子
static final float DEFAULT_LOAD_FACTOR = 0.75F;
// 当桶上的结点数大于这个值时会转成红黑树
static final int TREEIFY_THRESHOLD = 8;
// 当桶上的节点数小于这个值时会转成链表
static final int UNTREEIFY_THRESHOLD = 6;
// 当集合中的容量大于这个值时,表中的桶才能进行树形化,否则桶内元素太多时会扩容,而不是树形化          
// 为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD
static final int MIN_TREEIFY_CAPACITY = 64;
transient HashMap.Node<K, V>[] table;
transient Set<Entry<K, V>> entrySet;
// 记录集合被修改的次数
transient int modCount;
// 当前已占用数组长度
int threshold;
// 散列表的加载因子
final float loadFactor;

Hash算法

这种做法可以使数组元素分布均匀,减少散列冲突。

static final int hash(Object key) {
    int h;
    // 算出来的hash值比较分散,减少了碰撞的可能性
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

key在数组中的位置公式:i = (table.length - 1) & hash;

hash值算出来,要计算当前key在数组中的索引下标位置时,可以采用取模的方式,就是索引下标位置 = hash 值 % 数组大小,这样做的好处,就是可以保证计算出来的索引下标值可以均匀的分布在数组的各个索引位置上。但取模操作对于处理器的计算是比较慢的,数学上有个公式,当b是2的幂次方时,a % b = a &(b-1),所以此处索引位置的计算公式我们可以更换为: (n-1) & hash。

如何解决hash冲突

hash冲突指的是key值的hashcode计算相同,但key值不同的情况。

  • 好的hash算法
  • 到达阈值自动扩容
  • hash冲突发生时,采用链表来解决
  • hash冲突严重时,链表自动转化成红黑树,提高遍历速度

添加元素

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;
        // 如果key的hash和值都相等,直接把当前下标位置的Node值赋值给临时变量
        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);
                    // 当链表的长度大于等于 8 时,链表转红黑树
                    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;
        }
    }
    // 记录HashMap的数据结构发生了变化
    ++modCount;
    //如果HashMap的实际大小大于扩容的门槛,开始扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}
  1. 判断键值对数组长度是否是0,为0进行扩容。
  2. 根据键值key计算hash得到插入数据索引i,如果当前索引位置是空的,直接生成新的节点在当前索引位置上,转向6。如果不为空,转向3。
  3. hash冲突,两种解决方案:链表 or 红黑树。
  4. 如果是链表,调用链表的新增方法。
  5. 如果是红黑树,调用红黑树的新增方法。
  6. 通过onlyIfAbsent变量判断是否覆盖对应key的值。
  7. 判断是否需要扩容。

链表新增节点

循环链表,将新元素追加队尾。
当链表长度大于等于8,并且整个数组大小大于等于64时,才会转成红黑树。注意当数组大小小于64时,只会触发扩容,不会转化成红黑树。

红黑树新增节点

  • 通过equals判断新增的节点在红黑树上是不是已经存在;
  • 新增的节点如果已经在红黑树上,直接返回;不在的话,判断新增节点是在当前节点的左边还是右边,左边值小,右边值大;
  • 自旋当前节点,直到当前节点的左边或者右边的节点为空时,停止自旋,当前节点即为我们新增节点的父节点;
  • 把新增节点放到当前节点的左边或右边为空的地方,并于当前节点建立父子节点关系;
  • 进行着色,旋转。

扩容

扩容时机:

  1. 添加元素时发现数组为空,进行初始化扩容,默认扩容大小为 16;
  2. 添加元素成功,发现现有数组大小大于扩容的门阀值时,进行扩容,扩容为老数组大小的2倍;
    每次扩容时门阀值都会被重新计算,门阀值等于数组的大小 * 影响因子(0.75)。

1.7源码:
新建一个扩容数组,将数组元素转移到新数组里面,修改阈值。
转移数组元素:
遍历同桶数组中的每一个桶,遍历桶的外接链表。找到新表的桶位置。
旧桶数组中的某个桶的外挂单链表是通过头插法插入新桶数组中的,并且原链表中的Entry结点并不一定仍然在新桶数组的同一链表。

1.8源码:
为了性能在同一索引处发生哈希冲突到一定程度时链表结构会转换为红黑数结构存储冲突元素,故在扩容时如果当前索引中元素结构是红黑树且元素个数小于链表还原阈值时就会把树形结构缩小或直接还原为链表结构。

查找元素

通过key计算索引,找到桶位置,先检查第一个节点,如果是则返回,如果不是,则遍历其后面的链表或者红黑树,找到返回,没有找到返回null。

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

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                // 如果当前节点hash等于key的hash,并且equals相等,当前节点就是我们要找的节点
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

删除元素

删除元素首先是要找到桶的位置,然后如果是链表,则进行链表遍历,找到需要删除的元素后,进行删除;如果是红黑树,也是进行树的遍历,找到元素删除后,进行平衡调节,当红黑树的节点数小于6时,会转化成链表。

参考
JDK1.8源码(七)——java.util.HashMap 类
《极客时间-数据结构与算法之美专栏》

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343