Java集合(3):小白也能看懂的HashMap图解、底层原理与Hash算法

本文详细解析了Java中的HashMap,包括其无序、键唯一、值可重复的特点,以及基于哈希表和链表的底层数据结构。在JDK8后,HashMap引入了红黑树以优化长链表的查找性能。HashMap在多线程环境下不安全,可通过Hashtable、synchronizedMap或ConcurrentHashMap解决。文章还介绍了HashMap的属性,如默认容量、负载因子、树形化和取消树形化的阈值,并阐述了何时触发扩容和链表转红黑树。此外,详述了定位算法,即通过hash值与数组容量进行位与运算确定存储位置,以及JDK1.8中优化的hash算法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前面分析了Java集合中ArrayList和LinkedList的源码,这次说一下另一个常用的集合:HashMap。

一 、HashMap的特点

(1)属于Map下的集合,用KV键值对存储元素,元素是无序的,key不允许重复,value允许重复,允许存储null。
(2)底层数据结构是哈希表,实现是链表+数组,JDK 8 后又加了红黑树。
(3)多线程环境下不安全,解决方法:

  • 使用Hashtable;
  • 调用Collections类的synchronizedMap方法;
  • 使用juc包下的ConcurrentHashMap类代替(此方法效率最高)。

二、初识底层结构

特点中提到,HashMap底层结构为数组+链表+红黑树,先看一下大体的结构图:
在这里插入图片描述
简单的说,当一个数据要添加到HashMap中时,首先根据key找到数组的位置,如果数组已经有数据了则与前面的数据形成链表,如果链表过长则形成红黑树。
那么这个数组到底多大?链表多长时会形成红黑树?这一系列问题,我们可以在HashMap的属性中找到答案。

三、属性

HashMap中定义了六个常量,用来控制它的底层结构。
(1)static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
默认初始容容量等于16,也就是数组(也习惯称为为桶)的大小为16,这里既然有默认容量,也就是说数组的大小是可以改变的。此外,数组的容量必须是2的幂,至于原因会在后面解释。
(2)static final int MAXIMUM_CAPACITY = 1 << 30;
最大容量,也是说的数组长度(桶的个数)。为什么是1 << 30?因为int类型的数据能存下最大的2的幂就是2的30次方。
(3)static final float DEFAULT_LOAD_FACTOR = 0.75f;
默认的负载系数。前面说到数组的大小是会变化的,那么什么时候数组会变化呢?数组中已经存储的容量占总容量的负载系数倍,数组就会扩容。例如默认初始容量是16,默认负载系数为0.75,则当数组中存储元素超过16*0.75=12时,数组就会扩容。
(4)static final int TREEIFY_THRESHOLD = 8;
树形化的阈值为8。当一个链表上存储元素的个数多于8时链表就会开始转换为红黑树存储。
(5)static final int UNTREEIFY_THRESHOLD = 6;
取消树形化的阈值为6。当一个红黑树中的元素少于6时,红黑树就会转化为链表。
(6)static final int MIN_TREEIFY_CAPACITY = 64;
最小树形化阈值为64。这个参数的意思就是说,如果桶的个数小于64,那么即使链表长度大于8,也不会化为红黑树,而是会先采取扩容。

四、底层结构详解

看完了HashMap的各个属性,我们就可以明确HashMap底层结构的变化了:
1.如果构造方法采用默认的构造方法,会创建一个容量为16的数组。添加数据会在数组中添加,如果数组中有数,则在后面形成链表。
在这里插入图片描述

2.继续添加数据,有两种情况会导致数组扩容

a.HashMap中存储元素的个数大于阈值(数组容量*负载系数
在这里插入图片描述

b.如果数组其中一个格子的链表长度大于8
在这里插入图片描述

数组的扩容是按照扩容两倍的规则扩容的,扩容完后已有的数据会重新计算在HashMap中的位置。
3.扩容后的数组,如果容量大于64,继续添加数据,如果HashMap中存储元素的数量大于阈值(数组容量*负载系数)会继续扩容,但是如果链表长度大于8,链表转变为红黑树。
在这里插入图片描述
如果因为删除数据或扩容导致红黑树的元素小于6,红黑树会变回链表。关于添加数据、扩容等源码会在后面的文章详细介绍。

五、定位算法

看到这里,可能会疑惑,数据是如何选择要存到数组的哪个位置的?比较容易想到的是计算出数据key的hash值,与数组的容量进行取模(%)运算,然后得出位置。

其实HashMap也是这么做的,但是由于计算机的模运算消耗较大,HashMap采用了位与运算(&)来代替,用的是以下公式:hash % N = hash & N-1 。比如在HashMap中添加元素的方法中有段代码为:

 if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);

其中tab[]就是数组,i = (n - 1) & hash就是计算出的位置。

此外,在JDK1.8中还优化了hash算法,当数组容量太小时(例如16),参与位与运算(&)的hash值的高位并不会参加运算,决定存储位置只会取决于hash值的低四位,大大增大了hash冲突(多个数据进行计算存储的位置相同)的概率。优化的hash算法如下:

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

看代码可知是将hash值右移16位并进行异或(^)计算,这样高位也能参与后面的计算了。

综上,定位算法的计算过程如下:

在这里插入图片描述

### LinkedHashMap 数据结构解析 LinkedHashMapJava 集合框架中的一个重要组成部分,其设计目的是为了保持插入顺序或访问顺序。具体来说: #### 结构概述 LinkedHashMap 可以视为 HashMap 和 LinkedList 的组合体[^1]。这意味着 LinkedHashMap 不仅具备哈希表高效的查找性能,还利用双向链表来记录键值对的插入顺序。 #### 内部节点表示 在 LinkedHashMap 中,`LinkedHashMapEntry` 或 `Entry<K,V>` 类作为内部静态类存在,并扩展了 `HashMap.Node<K,V>` 。此条目不仅保存着键、值以及散列码的信息,而且引入了两个新的字段——`before` 和 `after` ,用于构建双端队列(Doubly Linked List),从而能够追踪元素之间的相对位置关系[^2][^3]。 ```java private static class Entry<K,V> extends HashMap.Node<K,V> { Entry<K,V> before, after; } ``` #### 插入遍历机制 每当有新元素被加入到 LinkedHashMap 实例中时,这些元素会按照它们进入集合的时间顺序链接起来形成一条单向链条;当迭代器遍历时,则可以从头至尾依次访问各个成员而不会打乱原有的次序[^4]。 #### 访问模式控制 通过调整构造函数参数 `accessOrder` (默认为 false 表示按插入顺序排列),还可以让 LinkedHashMap 支持 LRU 缓存策略下的最近最少使用淘汰算法 (Least Recently Used),此时每次读取某个特定项都会将其移动到列表末端以标记该条目的最新一次活跃状态。 --- ![image](https://upload.wikimedia.org/wikipedia/commons/thumb/7/7d/Java_util_LinkedHashMap.svg/800px-Java_util_LinkedHashMap.svg.png) 上图为 Wikimedia Commons 上的一个简单图解展示了 LinkedHashMap 如何结合 Hash Table 和 Doubly Linked List 来管理存储空间并维持元素间的逻辑连接。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值