HashMap关键源码分析及面试题

BAT面试题

1.HashMap的什么时候扩容,哪些操作会触发

        当向容器添加元素的时候,会判断当前容器的元素个数,如果大于等于阈值,即当前数组的长度乘以加载因子的值的时候,就要自动扩容。默认容量为16,扩容因子是0.75,阈值为12。

        有参构造方法和put、merge操作都会导致扩容。

2.HashMap push方法的执行过程? 

        最先判断桶的长度是否为0,为0的话则需要先对桶进行初始化操作,接着,求出hashcode并通过扰动函数确定要put到哪个桶中,若桶中没有元素直接插入,若有元素则判断key是否相等,如果相等的话,那么就将value改为我们put的value值,若不等的话,那么此时判断该点是否为树节点,如果是的话,调用putreeval方法,以树节点的方式插入,如果不是树节点,那么就遍历链表,如果找到了key那么修改value,没找到新建节点插到链表尾部,最后判断链表长度是否大于8 是否要进行树化。

3.HashMap检测到hash冲突后,将元素插入在链表的末尾还是开头? 

        因为JDK1.7是用单链表进行的纵向延伸,采用头插法就是能够提高插入的效率,但是也会容易出现逆序且环形链表死循环问题。但是在JDK1.8之后是因为加入了红黑树使用尾插法,能够避免出现逆序且链表死循环的问题。

4.1.8还采用了红黑树,讲讲红黑树的特性,为什么人家一定要用红黑树而不是AVL、B树之类的?

        在CurrentHashMap中是加锁了的,实际上是读写锁,如果写冲突就会等待,如果插入时间过长必然等待时间更长,而红黑树相对AVL树B树的插入更快,AVL树查询确实更快一些,但是对于操作密集型,红黑树的旋转更少,效率更高。

5.HashMap get方法的执行过程? 

        首先和put一样,确定对应的key在哪一个桶中,如果桶容量为0或者该桶内没有元素直接返回空,反之会判断该桶会检查桶中第一个元素是否和要查的key相等,相等的话直接返回,不相等的话判断该节点是否为树节点,是的话以树节点方式遍历整棵树来查找,不是的话那就说明存储结构是链表,以遍历链表的方式查找。

源码与其中的算法技巧

  1. 构造方法

public HashMap(int initialCapacity, float loadFactor) {


    //当指定的 initialCapacity (初始容量) < 0 的时候会抛出 IllegalArgumentException 异常
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);


    //当指定的 initialCapacity (初始容量)= MAXIMUM_CAPACITY(最大容量) 的时候 
    if (initialCapacity > MAXIMUM_CAPACITY)
        //初始容量就等于 MAXIMUM_CAPACITY (最大容量)
        initialCapacity = MAXIMUM_CAPACITY;


    //当 loadFactory(负载因子)< 0 ,或者不是数字的时候会抛出 IllegalArgumentException 异常
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;


    //tableSizeFor()的主要功能是返回一个比给定整数大且最接近2的幂次方整数
    //比如我们给定的数是12,那么tableSizeFor()会返回2的4次方,也就是16,因为16是最接近12并且大于12的数
    this.threshold = tableSizeFor(
        initialCapacity);
}

        执行顺序注释写的很清楚了,但是有些同学对最后对 tableSizeFor 方法很有疑问,这是用来求传入容量的最小2的幂次方整数的。

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

        这是一系列的或操作,举个例子

n-=1;// n=1000000(二进制)
...//16、8无变化
n|=n>>>4;//n=n|(n>>>4)=1000000|0000100=1000100
n|=n>>>2;//n=n|(n>>>2)=1000100|0010001=1010101
...

        看出规律来了吧,右移多少位,就把最高位右边的第x位设置为1;第二次,就把两个为1的右边xx位再设置为1;第n次,就把上一步出现的1右边xxxx位置为1;

这样执行完,原来是1000000,变成了1111111,最后加1,就变成2的整数次方数了。之所以先减一是因为有可能本身就是最小2的幂次方整数。

2.Put方法

        put方法的核心就是 putVal ,源码和执行过程如下。

//实现 put 和相关方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;


    //如果table为空或者长度为0,则进行resize()(扩容)
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;


    //确定插入table的位置,算法是上面提到的 (n - 1) & hash,在 n 为 2 的时候,相当于取模操作
    if ((p = tab[i = (n - 1) & hash]) == null)
        //找到key值对应的位置并且是第一个,直接插入
        tab[i] = newNode(hash, key, value, null);


    //在table的 i 的位置发生碰撞,分两种情况
    //1、key值是一样的,替换value值
    //2、key值不一样的
    //而key值不一样的有两种处理方式:1、存储在 i 的位置的链表 2、存储在红黑树中
    else {
        Node<K,V> e; K k;


        //第一个Node的hash值即为要加入元素的hash
        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 {
            //如果不是TreeNode的话,即为链表,然后遍历链表
            for (int binCount = 0; ; ++binCount) {


                //链表的尾端也没有找到key值相同的节点,则生成一个新的Node
                //并且判断链表的节点个数是不是到达转换成红黑树的上界达到,则转换成红黑树
                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;
            }
        }


        //如果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;
}

        为了方便理解,配图

putVal中有一段代码提到了resize(),也就是扩容,我们来看下源码

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;


    //判断Node的长度,如果不为零
    if (oldCap > 0) {
        //判断当前Node的长度,如果当前长度超过 MAXIMUM_CAPACITY(最大容量值)
        if (oldCap >= MAXIMUM_CAPACITY) {
            //新增阀值为 Integer.MAX_VALUE
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }


        //如果小于这个 MAXIMUM_CAPACITY(最大容量值),并且大于 DEFAULT_INITIAL_CAPACITY (默认16)
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
            //进行2倍扩容
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        //指定新增阀值
        newCap = oldThr;


    //如果数组为空
    else {               // zero initial threshold signifies using defaults
        //使用默认的加载因子(0.75)
        newCap = DEFAULT_INITIAL_CAPACITY;
        //新增的阀值也就为 16 * 0.75 = 12
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {
        //按照给定的初始大小计算扩容后的新增阀值
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }


    //扩容后的新增阀值
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    //扩容后的Node数组
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;


    //如果数组不为空,将原数组中的元素放入扩容后的数组中
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;


                //如果节点为空,则直接计算在新数组中的位置,放入即可
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    //拆分树节点
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    //如果节点不为空,且为单链表,则将原数组中单链表元素进行拆分
                    Node<K,V> loHead = null, loTail = null;//保存在原有索引的链表
                    Node<K,V> hiHead = null, hiTail = null;//保存在新索引的链表
                    Node<K,V> next;
                    do {
                        next = e.next;


                        //哈希值和原数组长度进行&操作,为0则在原数组的索引位置
                        //非0则在原数组索引位置+原数组长度的新位置
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;



为什么会hash冲突?

        就是根据key即经过一个函数f(key)得到的结果的作为地址去存放当前的key,value键值对(这个是hashmap的存值方式),但是却发现算出来的地址上已经有数据。这就是所谓的hash冲突。

hash冲突的几种情况:

        1两个节点的key值相同(hash值一定相同),导致冲突 

        2 两个节点的key值不同,由于hash函数的局限性导致hash值相同,导致冲突 

        3 两个节点的key值不同,hash值不同,但hash值对数组长度取模后相同,导致冲突 

        

        如何解决hash冲突?解决hash冲突的方法主要有两种,一种是开放寻址法,另一种是链表法 。


开放寻址法--线性探测

        开放寻址法的原理很简单,就是当一个Key通过hash函数获得对应的数组下标已被占用的时候,我们可以寻找下一个空档位置

        比如有个Entry6通过hash函数得到的下标为2,但是该下标在数组中已经有了其它的元素,那么就向后移动1位,看看数组下标为3的位置是否有空位

        但是下标为3的数组也已经被占用了,那么久再向后移动1位,看看数组下标为4的位置是否为空

        数组下标为4的位置还没有被占用,所以可以把Entry6存入到数组下标为4的位置。这就是开放寻址的基本思路,寻址的方式有很多种,这里只是简单的一个示例

链表法

        链表法也正是被应用在了HashMap中,HashMap中数组的每一个元素不仅是一个Entry对象,还是一个链表的头节点。每一个Entry对象通过next指针指向它的下一个Entry节点。当新来的Entry映射到与之冲突的数组位置时,只需要插入到对应的链表中即可

额…… 写不完了 其他操作下篇继续 

◆ ◆ ◆  ◆ ◆

关注并后台回复 “面试” 或者  “视频”,

即可免费获取最新2019BAT

大厂面试题和大数据微服务视频

您的分享和支持是我更新的动力

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

后端开发技术

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

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

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

打赏作者

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

抵扣说明:

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

余额充值