面试官!你又双叒叕问HashMap!

HashMap的数据结构(图解+源码分析)

HashMap快速索引

数组

定义:连续的内存,具有共同特性的数据

现有一个8个元素的空数组,如下图
在这里插入图片描述
向数组尾部添加数据:
在这里插入图片描述
向数组中插入节点:

如果项目把475插入到下标为1的位置,就要把275的下标位置向后移一位
在这里插入图片描述
优点:连续内存,通过数组下标可以快速寻址

缺点:中间插入节点比较慢

单链表

定义:任意内存单元通过指针连接每个结点,存放线性表中的数据

现有如下三个结点的单链表:
在这里插入图片描述
向单链表尾部添加数据:把next指针指向下一个节点的Data域
在这里插入图片描述
向点链表中插入节点:

假设在“275”和“297”之间插入新的结点,那么只需把“297”的next指向新结点的data域,然后新结点的next指向“297”的data域。无需同数组那样调整后面结点的位置。
在这里插入图片描述
优点:插入和删除数据方便

缺点:单链表查询某个结点,需要从头结点开始遍历整个链表,所以单链表的查询效率比较低

HashMap如何插入数据(图解+源码分析pos)

建立插入值和数组所在下标的位置(pos = key % size key: 插入数据的内容;size:数组的大小

比如要将100,201,303这三个数据插入到一个长度为10的以链表特性声明的数组

pos1 = 100 % 10 = 0

pos2 = 201 % 10 = 1

pos3 = 303 % 10 = 3
在这里插入图片描述
HashMap的构造函数:

/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;

public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

HashMap插入函数(put):

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
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;
            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);
                        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;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

其中table是 “transient Node<K,V>[]”单链表结构的数组

其中resize()

/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;

else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;//16
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//12
        }

threshold = newThr;//12

为什么初始化容量是2的倍数(源码分析)

else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
  1. 计算机申请内存是2的倍数,可以避免内存碎片

  2. 移位操作效率高

  3. 参与到hash运算中提高散列度

    putVal()中:

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

    举例:

    n = 16 (10000)

    i=(n-1) : 0000 1111

    hash1 : 0000 0001 -> 0000 0001

    hash2 : 0001 0000 -> 0000 0000

    若:n = 15 (01111)

    i=(n-1) : 0000 1110

    hash1 : 0000 0001 -> 0000 0000

    hash2 : 0001 0000 -> 0000 0000

    所以HashMap初始化数组使用2的倍数(因为2的倍数减1,后面的全1)可以提高散列度和降低冲突

HashMap如何解决Key冲突(图解+源码分析)

上一部分,咱们插入的数据是没有冲突的,如果我们插入200,那么pos取模后也是0,和100这个结点冲突了,HashMap针对这种冲突问题,通过单链表解决
在这里插入图片描述
查询到0的时候,就可以定位到100和200了,

但是问题来了,如果以这种单链表的形式解决冲突问题,那么查询起来会很慢!

为了解决上面的这个问题JDK1.8中引入了“红黑树”(高度平衡的平衡二叉树)这一概念,因为二叉树的查询效率是树的深度,显然查询效率比单链表高。
在这里插入图片描述

HashMap如何扩容(源码分析)

HashMap查询与删除

HashMap序列化(源码分析)

总结

  1. HashMap的数据结构包括数组、单链表、红黑树;

  2. 数组容量为2的倍数:

    ​ a.提高运算速度,

    ​ b.增加散列度,降低冲突,

    ​ c.减少内存碎片 。

  3. 通过hash函数和pos = key % size 确定数据插入位置,hashcode的高16位和低16位进行异或取模,增加散列度,降低冲突;

  4. 当两个及两个以上的key相同且data不同的时候(插入冲突),通过单链表解决冲突,如果链表的长度超过(TREEIFY_THRESHOLD = 8), 将单链表转化成红黑树以提高查询效率;

  5. 扩容条件:实际节点数大于等于容量的3/4;扩容后的数据排布:1.原下标的位置;2.原下标+原容量的位置

  6. 序列化:只存储了数组的容量、实际结点数量和各个结点的key,value值.

还有 HashMap如何扩容(源码分析) 、 HashMap查询与删除 、HashMap序列化(源码分析)这三部分在整理中~觉得看完有帮助的小伙伴,可以关注小名或者收藏此文等待后期更新哈😁

评论 69
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

进阶的小名

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值