HashMap数据结构剖析,看完这篇,面试它不再是拦路虎

引言:是否还在面试过程中被问到HashMap数据结构而被难住,来看看这篇吧!如有不足敬请斧正!

1.数据结构模型

数组 + 链表 + 红黑树(JDK 1.8引入)
初始化时为数组,插入时存在冲突,引入链表,链表查询慢引入红黑树。

2.JDK1.8引入红黑树只是为了效率吗?

不仅仅。HashMap会导致DOS攻击,可以搜索:线程安全的场景下;CVE-2011-4858;Tomcat邮件组的讨论时间。查看一种:没想到 Hash 冲突还能这么玩,你的服务中招了吗? - 开发者头条 。 如果黑客在get后大量拼接hash冲突的键值对,tomcat键值存储时,形成链表,查询聊表效率低下,分分钟让CPU爆掉宕机!(扩展:服务器出现DOS,cpu 100%。kill -9不合适,重启不合适,别人依旧会再次攻击,top、jstack、jmap、atrhas命令,JVM性能调优,JVM性能排查,确定哪个线程,哪段代码有问题)

JDK8前为了避免上述问题,tomcat给参数长度设置了最大长度,JDK8后引入红黑树避免链表长度过长。

对于链表的查询效率,可以简单的使用:List<String> list = new ArrayList<>();List<String> list = new LinkedList<>();测试效率。

/***
 *  HashMap先比较hashcode,然后比较equals方法看是否是同一个对象。是就覆盖key的旧值,否则链表新插入。
 *  hashcode相同,且equals为true
 */
@Test
public void hashMap(){
    Map<String,String> hashMap = new HashMap<>();
    List<String> list = Arrays.asList("Aa","BB","C#");
    for(String s : list){
        //2112  2112   2112  哈希值相同,构成链表
        System.out.println(s.hashCode());
        hashMap.put(s,s);
    }
    for(String key : hashMap.keySet()){
        //Aa,Aa    BB,BB   C#,C#
        System.out.println(key + "," + hashMap.get(key));
    }
}

3.为什么是红黑树?

红黑树,属于二叉树,是二叉排序树,为什么不是平衡二叉树呢?树里面查询效率最高的是完全平衡的二叉树,但是比如10.11.12,11在10右边,12在11右边,这不是树,组成树树结构形式下那么10在11左下边,但是树的旋转也是十分消耗性能,红黑树尽量减少旋转属于折中方案,遵守最小长度min和最长长度:max <= 2min。

4.HashMap的哈希方法

Object的hashCode方法,调用了native本地(jvm实现,下载hotspot源码可查看)的hashCode(),
而HashMap定义了hash方法:

/***
 * HashMap的哈希方法
 * static final int hash(Object key) {
 *    int h;
 *    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
 * }
 * 拆解:
 */
public int hash(Object key){
    int h;
    if(key == null){
        return 0;
    }else{
        h = key.hashCode();//哈希值,int 4个字节32位
        int temp = h>>>16; //右移16位
        int newHash = h ^ temp;  //异或运算(不同为1)
        return newHash;
    }
}

 代码的拆解:

hash = hash(Object key)=hashCode^(hashCode>>>16)是为了什么?

查看源码,是为了计算下标,得到的值作为数组的定位,hash & (n-1)得到数组下标。

为啥hash & (n-1)这样计算可以作为数组下标?

一般计算下标的方法:hash%n取模,n为数组长度。但是这样需要多次运算,让得到的结果小于n。HashMap中对这种处理做了优化。

hash & (n-1)与hash%n为啥可以等效?

等效的前提是n为2的n次幂,HashMap的初始数组容量为16=2^4=10000,(n-1)=15=01111.
hash&(n-1)=hash&00000000000000000000000000001111,在与运算中,前28位无效,与之后的结果肯定是0,那么看低4位,所以hash&(n-1)的结果就是(0000,1111)=(0,15)正好是下标的取值范围。

为什么要右移16位?

让高位也参与运算,避免比较大的数据,高位不同,低位相同,那么下标都是低位计算,都是一样的。

为什么要用异或?

假如用&与,或者用非|,和异或^进行概率统计:

可以得出结论:HashMap的hash()算法是为了计算数组定位,避免hash冲突,使计算的数组下标更均匀,从而链表比较少,更高效。

5.数组的容量为什么是2的整数次幂?

h(k) = k mod m,除法散列发,在《算法导论》中,推荐我们m不应是2的整数幂,因为m=2^p,则h(k)就是k的p个最低位数字,除非我们已经知道各种最低P位的排列是等可能的,否则我们最好慎重的选择m,而一个不太接近2的整数幂的素数,往往是较好的选择。
因为在hash算法中已经使用了hash^(hash>>>16)优化,为了效率的同时,违背了算法导论。
在hashtable中,初始容量是11,遵循了算法导论。

6.扩容因子(元素的填充因子、加载因子 )为什么是0.75?

扩容因子0.75就是元素占用率达到0.75,数组长度就扩容。
HashMap数组初始大小16,但是hash算法就是为了避免哈希冲突,不让产生大量的链表和红黑树。
扩容因子为1,空间利用率很高,但是意味填满了,很有可能有几个形成了链表,查询成本高,扩容因子为0.5利用率太低,综合考虑选取0.75。

7.HashMap树化参数为什么是8?

即链表大于等于8,转化成红黑树。
泊松分布:链表元素长度为7(7个元素出现哈希冲突)的概率是0.00000094,为8的概率是0.00000006,树化参数很小,转化红黑树资源消耗大。但是链表长度达到8,一定会树化吗?不是,源码里还有一个最小数化容量参数,值为64,就是要扩容到64,且长度大于等于8才会树化。

8.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;
    //tab为空时候,resize初始化或者扩容
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    //(n - 1) & hash 就是上面说的计算小标,判断下标有没有值
    if ((p = tab[i = (n - 1) & hash]) == null)
        //没有哈希冲突,直接存入
        tab[i] = newNode(hash, key, value, null);
    else {
        //哈希冲突
        Node<K,V> e; K k;
        //p是原有数组,特例:两个相同的key就会走这儿。
        if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))
            //原来的值赋值给e
            e = p;
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            for (int binCount = 0; ; ++binCount) {
                //p后面没有值
                if ((e = p.next) == null) {
                    //p指向新的后面的节点
                    p.next = newNode(hash, key, value, null);
                    //判断是否需要要树化,判断长度是否大于等于8
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        //要判断数组长度是否大于64
                        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;
            //onlyIfAbsent 是false,旧值也不是null
            if (!onlyIfAbsent || oldValue == null)
                //新的value覆盖旧的value。
                e.value = value;
            afterNodeAccess(e);
            //hash冲突了,新的值覆盖旧的,且返回旧值。
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

9.扩容机制,扩容后数组怎么定位?


如上,哈希为4或者20,长度为16,取余( 或者上述的hash&(n-1) )数组下标都是4,扩容成32长度数组,则分别挂在4和20下标下-->扩容后的数组下标要么为原来位置,要么为原来位置+16。

为什么呢?
原hash&(16-1)更换为现在hash&(32-1)=hash&000000000000000000000000000011111,原来计算低四位,现在换成低5位,hash倒数低5位为1,则相当于原位置+10000即+16,为0则对结果没有影响就还是原位置。(扩容是为了让元素更分散)

10.为什么HashMap线程不安全?

多线程出现循环链表,尾指针指向上一个节点的头;
此时推荐使用ConcurrentHashMap;
为什么不用Map加锁?加锁让多线程变成了单线程,效率。concurrentHashMap没有用锁,CAS机制,原子操作。

完结!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值