HashMap 面试中的 12 个点

一. 你知道哪些 map

HashMap, TreeMap, ConcurrentHashMap, LinkedHashMap

二. HashMap 的特点是什么?

  1. 允许 KeyValuenull ,不过只能有一条记录 Keynull

  2. 线程不安全

  3. 无序

  4. 数据结构是 数组+链表/红黑树(JDK1.8)

三. JDK1.8 中 HashMap 为什么要引入红黑树 ?

链表 查询时间复杂度 O(n) , 插入时间复杂度 O(1)

红黑树 查询和插入时间复杂度都是 O(lgn)

四. HashMap长度为什么只能是 2 的倍数

  1. 计算 Hash 值时采用位运算来代替取模,能更高效地计算出元素的位置。

元素对应的 index 是通过下面代码赋值的 ,即 index = hash & hashmap的table长度-1

if ((p = tab[i = (n - 1) & hash]) == null)
  1. 扩容时能更快地计算出 keyindex,提高扩容效率

  2. 比如 map 大小为 16 ,key 为 2 所在的 index 为 2, 18 所在的 index 也是 2

  3. 但是扩容之后变成 32 了,key 为 2 所在的 index 还是为 2, 而 18 所在的 index 就变为· 2+16=18 了。

这里 4ye 就直接截取了 resize 中 链表 重新计算 index 的部分,红黑树 TreeNode 中也有类似的代码

五. HashMap什么时候进行扩容

当( hash 表的大小),> ( hash 表的大小 * 负载因子)的值时,则进行扩容

六. 负载因子的大小

负载因子大小决定着 哈希表的扩容哈希冲突 ,每次 put **新元素 **后都要检查,java培训看看需不需要扩容,扩容默认是原来的两倍。

调高负载因子,会增加 hash 冲突的概率 ,同样会增加耗时,扩容本身也会耗时。

七. 怎么计算 hash 值

计算 key 的 hashCode 值,然后将这个值与它的高十六位进行异或运算

这么做是为了减少 hash 冲突

//      >>> 无符号位右移,即不管该数的正负,都在高位补0//      >> 表示右移,如果该数为正,则高位补0,若为负数,则高位补1//      << 左移直接在低位补0,无正负之

验证下~

int hashCode = "Java4ye".hashCode();System.out.println((hashCode ^ (hashCode >>> 16))&15);// 打印出 0hashMap.put("Java4ye","1")

八. hash 冲突的解决办法

  1. 开放定址法

  2. 链地址法 (拉链法) HashMap 采用的

  3. 再哈希法

  4. 公共溢出区域法

九. put 和 get 的实现

put 时

先计算该 key 的 hash 值,并算出它 所在的 bucket 的 index ,如果没有碰撞的话,直接放到数组中

如果碰撞了,先判断,这个 key 是不是同一个 key,是的话直接覆盖

不是的话 再去判断当前是 链表 还是 红黑树,再依据不同的情况进行插入,如果 key 一样的话,会根据 onlyIfAbsent 的值 或者 原来的值是否为 null 进行替换或者保留原来的值。

如果是找到对应的 key 的话,会返回该旧值,不会继续往下执行

如果是增加新元素的话,最后会判断 hash 表的大小,如果 该值 大于 hash 表的大小 * 负载因子,则进行扩容;最后返回 null

put 源码:

/** * Implements Map.put and related methods * * @param hash hash for key * @param key the key * @param value the value to put * @param onlyIfAbsent if true, don't change existing value * @param evict if false, the table is in creation mode. * @return previous value, or null if none */final V putVal(int hash, K key, V value, boolean onlyIfAbsent,               boolean evict) {    Node<K,V>[] tab; Node<K,V> p; int n, i;    // 创建Hashmap时是没有去初始化 bucket 的容量的,在put操作时才去扩容    if ((tab = table) == null || (n = tab.length) == 0)         n = (tab = resize()).length;    // 这里看看对应的 index 有没有数据,没有就直接放到这里    if ((p = tab[i = (n - 1) & hash]) == null)        tab[i] = newNode(hash, key, value, null);  //  有数据的话,就进入下面去判断,先判断是不是同个key,再判断属于哪种数据结构,红黑树或者链表    else {        Node<K,V> e; K k;        // 判断是不是同个key        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 {            // 链表,这里采用的是尾插法!在链表中查找该key,有的话break,没有的话一直找,直到链表中最后一个元素,并加入链表尾部            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;                }                // 判断是不是同个key,是的话跳过                if (e.hash == hash &&                    ((k = e.key) == key || (key != null && key.equals(k))))                    break;                p = e;            }        }        // e 有值表示 当前 key 有对应的值 如果 onlyIfAbsent 的值为 false 或者当前的 value 为 null 时 则进行覆盖 , 然后 return 该 旧的值。        if (e != null) { // existing mapping for key            V oldValue = e.value;            if (!onlyIfAbsent || oldValue == null)                e.value = value;            afterNodeAccess(e);            return oldValue;        }    }    // 如果 put 的是一个新元素,则会来到这一步,进行 +1 ,判断是否需要扩容    ++modCount;    if (++size > threshold)        resize();    afterNodeInsertion(evict);    return null;}

get 的时候也要先计算 key 的 hash 值,然后算出它的 index,接着判断有没有 hash 冲突,没有冲突的话,直接返回,有的话需要判断当前的数据结构是链表还是红黑树,然后分别从相应的结构中取出值

这图片来自 美团技术团队 ~

十. 怎么判断一个元素是否相同的呢

先判断他们的 hash 值是否相同,相同的话再判断该 key 的 equals 方法 || == 方法是否相同,相同的话则是同一个元素

十一. 什么情况下会用到红黑树?

可以参考上面的 put 源码 ,主要代码如图:

可以看到当数组的大小大于 64 且链表的长度大于 8 时,会将链表转换成红黑树。

当红黑树的大小小于等于 6 时,会转换成链表,参考 resize 源码

十二. 头插法和尾插法

由于 jdk1.7 中 HashMap 使用的是头插法, 那么新元素总会被放在链表的头部

比如 HashMap 大小为 4, key 2,6 ,10 所在的 index 都是 2

那么当它 扩容的时候,顺序就会变成

index:2,key:10 ,2

index:6,key:6

在多线程环境下,有可能会导致死循环~

比如 线程 a 还停留在 2.next 6,6.next 10 ,线程 b 已经完成 resize 了,变成了 10.next2 这时就会进入死循环了。

这也是 HashMap 线程不安全的原因之一。

同样的例子:尾插法不会导致死循环

如:线程 a 还停留在 2.next 6,6.next 10 ,线程 b 已经完成 resize 了,变成了 2.next10 了。

使用尾插法并不意味着 HashMap 就是线程安全了,因为你无法保证 put 进去的值,get 出来的还是 put 进去的值,因为有可能已经被别的线程修改了。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值