HashMap 详解

HashMap 详解

基于 哈希表 的 Map<K, V> 接口的实现。

此实现提供所有可选的 映射 操作,并允许使用 null 值和 null 键。

是非线程安全的实现。


1、JDK7 和 JDK8 的 HashMap 的区别?
JDK7:

1. 底层实现

  基于 数组 + 链表 来实现,它的底层维护一个 Entry 数组,它会根据计算的 hashCode 将对应的 KV 键值对 存储到该数组中。

2. 解决 Hash 冲突的方法

  将该 KV 键值对 放到对应的已有元素的后面,形成了一个 链表式 的存储结构。

缺点:当 Hash 冲突 严重时,在桶上形成的链表会变得越来越长,这样在查询时的效率就会越来越低,其时间复杂度为O(N)。

JDK8:

1. 底层实现

  是基于 数组 + 链表 + 红黑树 来实现的,它的底层维护一个 Node 数组,当链表的存储的数据个数 大于等于8 的时候,不再采用 链表 存储,而采用了 红黑树 存储结构。

2. 解决 Hash 冲突的方法

  当链表长度到达一个阈值时,会将 单向链表 转换成 红黑树 提高性能。而当链表长度缩小到另一个阈值时,又会将 红黑树 转换回 单向链表 提高性能。

优点:大大的提高了 查找 性能,链表 为 O(N),而 红黑树 是 O(logN)。



2、HashMap 底层的实现原理

  ​ 它基于 hash 算法,通过 put 方法 和 get方法 存储 和 获取对象。

存储对象 :我们将 K/V 传给 put 方法 时,它调用 K 的 hashCode 方法 计算 hash 值 从而得到 bucket位置,进一步存储,HashMap 会根据当前 bucket 的占用情况自动调整容量(超过 Load Facotr 则resize为原来的2倍)。

  ​获取对象: 我们将 K 传给 get 方法 时,它调用 K 的 hashCode 方法 计算 hash 值 从而得到 bucket 位置,并进一步调用 equals() 方法 确定键值对。

  如果发生碰撞的时候,HashMap 通过 链表 将产生碰撞冲突的元素组织起来。在 JDK8 中,如果一个 bucket 中碰撞冲突的元素超过某个限制(默认是8),则使用 红黑树 来替换 链表,从而提高速度。

bucket 是什么?

    bucket哈希桶数组:Node<K,V>[] table,Node 是 HashMap 的一个 内部类,实现了Map.Entry 接口。


源码:

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;

    Node(int hash, K key, V value, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }
}

transient Node<K,V>[] table;

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}


3、HashMap 为什么使用 红黑树 而不用 B树 ?

  B/B+ 树 多用于 外存 上,B/B+ 树 也成为了一个磁盘友好的数据结构。(MySQL 底层就是用 B+ 树实现的)

  如果用 B/B+ 树 的话,在数据量不是很多的情况下,数据都会 “挤在” 一个结点里面,这个时候遍历效率就退化成了 链表。

什么是 B树 ?

    B树 是一种 平衡的多路查找树。一个节点可以存入多个值(当数据量较多时,可以提高遍历效率;但当数据量比较少时,遍历效率反而比较低)。



4、HashMap 为什么 线程不安全?

  HashMap 在并发执行 put 操作时,可能会导致形成 循环链表,从而引起死循环。



5、HashMap 的 循环链表 是怎么产生的?

  导致死循环的根本原因是 JDK7 扩容采用的是 “头插法”,会导致同一索引位置的节点在扩容后顺序反掉。而 JDK8 之后采用的是 “尾插法”,扩容后节点顺序不会反掉,理论上不存在死循环问题,但实际上依然存在死循环问题。



6、HashMap 是怎么 扩容 的?
  1. 数组的 初始容量为 16,而容量是以 2 的次方 扩充的,一是为了提高性能使用足够大的数组,二是为了能使用 位运算 代替 取模预算(据说提升了5~8倍)。
  2. 数组是否需要扩充 是通过 负载因子 判断的,如果当前元素个数为数组容量的 0.75 时,就会扩充数组。这个 0.75 就是默认的负载因子,可由构造器传入。我们也可以设置大于 1 的负载因子,这样数组就不会扩充,牺牲性能,节省内存。
  3. 为了解决碰撞,数组中的元素是 单向链表 类型。当链表长度到达一个阈值时(7 或 8),会将链表 转换成 红黑树 提高性能。而当链表长度缩小到另一个阈值时(6),又会将 红黑树 转换回 单向链表 提高性能。
  4. 对于第三点补充说明,检查 链表长度 转换成 红黑树 之前,还会先检测 当前数组容量 是否到达一个阈值(64),如果没有到达这个容量,会放弃转换,先去 扩充数组。所以上面也说了链表长度的阈值是 7 或 8,因为会有一次放弃转换的操作。

补充:扩容阈值(threshold) = 容量 * 负载因子(loadFactor)


事例:
例如:容量从 16 扩容到 32 ,具体的变化如下所示:
在这里插入图片描述
因此元素在重新计算 hash 之后,因为 n 变为 2 倍,那么 n-1 的 mask 范围在高位多 1bit(红色),因此新的 index 就会发生这样的变化:
在这里插入图片描述
因此,我们在扩充 HashMap 的时候,不需要重新计算 hash,只需要看看原来的 hash 值 新增 的那个 bit 是 1 还是 0 就好了,是 0 的话 索引没变,是 1 的话 索引变成 “原索引 + oldCap”。可以看看下图为 16 扩充为 32 的 resize 示意图:
在这里插入图片描述
这个设计确实非常的巧妙,既省去了重新计算 hash 值 的时间,而且同时,由于新增的 1bit 是 0 还是1 可以认为是随机的,因此 resize 的过程,均匀的把之前的冲突的节点分散到新的 bucket 了。



部分资料来源:
https://www.nowcoder.com/tutorial/94

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值