HashMap源码面试题

总体特点:

  1. java1.8之前使用的是数组+单向链表的数据结构,java1.8及之后使用的是数组+单向链表+红黑树的数据结构,在hash冲突形成的单向链表元素个数达到8个时会转成红黑树。
  2. 使用无参构造器初始化的node数组容量是16(在resize()方法中初始化),加载因子是0.75(也就是说当node数组中元素超过0.75容量时会进行扩容),扩容时新容量是旧容量的2倍。
  3. 扩容时java1.8之前使用的头插法,头插法可能会导致元素的丢失、hash环等问题,java1.8及之后使用的是尾插法,能够避免之前版本扩容时存在的问题
  4. HashMap中的元素不保证有序性,跟元素插入顺序会不一致
    实战经验及使用场景:
  5. HashMap作为key-value形式最常使用的集合,是一个线程不安全的集合,一般情况不建议使用在类成员变量中,如果要用作类成员变量,那么正确使用方式是用static代码块或构造器执行put操作,要不就是在put时使用lock或sycsynchronized使得put元素过程是单线程操作。
  6. 在初始化HashMap时最好初始化大小,防止频繁扩容
  7. hashmap如果做为类成员变量储存数据,则要考虑key清除策略,要不可能会导致内存泄漏
  8. HashMap中的元素不保证有序性,跟元素插入顺序会不一致,如果碰到需要加密或签名等场景则需要使用linkedHashMap
    一些显示技术能力及有思考的面试题:
    1.为什么用(h = key.hashCode()) ^ (h >>> 16)算hash?
    答:降低hash冲突的概率。比如key=”张三“。
    key.hashCode() 为:
    1111 1111 1111 1111 1111 1010 0111 1100
    此时的node数组大小为16,那么(n - 1) & hash会是下面的操作:
    1111 1111 1111 1111 1111 1010 0111 1100
    0000 0000 0000 0000 0000 0000 0000 0111
    那么实际上”张三“这个key的hashcode只有后三位会影响算数组的位置,其他位都是没影响的。
    那么如果使用(h = key.hashCode()) ^ (h >>> 16)呢?
    1111 1111 1111 1111 1111 1010 0111 1100
    0000 0000 0000 0000 1111 1111 1111 1111
    1111 1111 1111 1111 0000 0101 1000 0011
    那么最后进行(n - 1) & hash操作的hashcode就变了,使得之前没影响的高16位的hash也参与了数组位置的决定,这就会舍得hashcode冲突的概率降低。
    (n - 1) & hash为什么能保证数组下标不能越界呢?
    答:这就跟hashMap容量有关,hashMap巧妙的利用了2的幂方来作为容量,默认情况下初始容量为16,之后扩容是旧容量的2倍。我们以16为例:16-1 =15,写成二进制:1111,这种二进制跟任何一个二进制取&都会小于15,这就相当于%操作,但&的效率会高很多。下标现在是可以确定了,但如果元素的hash值不够散列,就会造成hash碰撞问题,hash值相同的key会以单链表或红黑树的形式储存。Hash碰撞比较严重的话会严重影响hashMap查询和插入的性能,所以应该尽量使hash值随机。
    长度为什么是的2的幂次方
    答:1. 可以使用(n - 1) & hash方式确定下标,效率更高
  9. 扩容时重新计算位置会更加简单(针对有hash冲突情况),扩容后新容量是旧容量的2倍,相当于原二进制向左移动一位,比如之前容量是16,二进制是1 0000,扩容后是32,二进制是
    10 0000.也就是元素的高位部分最后一个会参与(n - 1) & hash位置计算,而使用hash&oldCap方式就能确定参与运算的高位会不会改变原来的位置.当参与计算的高位是1时,新位置=j+oldCap,当参与计算的高位是0时,新位置=j。这里涉及的代码逻辑在resize()方法中:

1.8如何解决了1.7之前会造成死循环的问题?
答:

  1. 1.7会造成死循环的由来
    比如现在hashmap中的table是这样的

其中key:张三,李四,王五hash冲突了形成了一个单链表,正常根据上面的代码会重新定位node在新数组中的位置在放到新数组中,此时我们再假设这三个key在新数组中还是hash冲突了,此时数组就变成了:

这就是所谓的头插法,在单线程扩容的看起来一点问题没有,但多个线程进行扩容就不行了,比如现在有线程a和线程b来扩容
线程a先在正常执行上诉扩容的逻辑,并且扩容到了如下步骤:

此时线程b发现也需要扩容,他也开始进行扩容执行上诉逻辑,那么他开始的时候:
e="李四“
next="张三”
table[i]="李四“
然后执行e.next = newTable[i]就变成"李四“.next = ‘’"李四“
newTable[i] =‘’"李四“;

经过线程b的完整扩容之后就变成了这样,当执行get王五时就死循环了。
2. 1.8如何解决死循环
从上面可以看出,形成环的主要原因是要把单链表倒置,也就是头插入,如果是尾插入就不会改变next指针的指向。也就不会形成环状链表,所以1.8使用了尾插入,而且使用尾插入还有一个好处是hash冲突元素扩容新位置会很好计算,上面已阐释,这里就不展开说明了。
1.8 还有什么线程安全问题?
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null)
可能会形成数据覆盖,线程a和线程b同时执行(p = tab[i = (n - 1) & hash]) == null)且i是一样的,之后线程a挂起,线程b执行,之后线程a再执行就会覆盖线程b的元素。
++size
可能使size跟实际size不一样。

欢迎关注公众号:java面试工程师

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值