目录
1.构造一个空的 HashMap ,默认初始容量(16)和默认负载因子(0.75)。
2.构造一个具有指定的初始容量和默认负载因子(0.75) HashMap。
3.构造一个具有指定的初始容量和负载因子的 HashMap。
HashMap基本概念
什么是HashMap
HashMap是Java中基于哈希表实现的Map接口的一个非同步实现类,是以key-value存储形式存在,即主要用来存放键值对。它的key、value都可以为null。HashMap 的实现不是同步的,这意味着它不是线程安全的。此外,HashMap中的映射不是有序的。
HashMap的特点
下面是HashMap的一些基本特点,具有这些特点的原因会在后面详细解读:
-
存取无序:元素的存储和访问不保证任何顺序。
-
键和值:可以存储空值(
null
),但只能有一个空键。 -
键的唯一性:底层数据结构确保键的唯一性。
-
数据结构演变:在JDK 1.8之前,
HashMap
使用链表和数组来存储数据。JDK 1.8之后,引入了红黑树以优化性能。 -
转换为红黑树:当链表长度超过一定阈值(默认为8)且数组长度大于64时,链表会转换为红黑树,以提高查询效率。这一转换是为了在面对大量数据时,保持
HashMap
操作的高效性。
HashMap类的继承和实现关系
HashMap
在Java集合框架中的继承和实现关系如下:
-
HashMap
继承自AbstractMap
类,这个抽象类提供了Map
接口的大部分实现。 -
AbstractMap
实现了Map
接口,该接口定义了映射表的基本操作。 -
HashMap
实现了Cloneable
接口,允许HashMap
对象被克隆。 -
HashMap
同样实现了Serializable
接口,使得HashMap
对象可以被序列化。
这种设计使得
HashMap
具备了Map
接口的功能,并且能够进行对象克隆和序列化操作。
深入了解HashMap前需要知道
hashCode()和equals()方法的关系
在Java中,hashCode()
和equals()
方法在集合类(例如HashMap
、HashSet
)中扮演着至关重要的角色,它们共同决定了对象的逻辑相等性以及在哈希存储结构中的处理方式:
-
equals()方法:用于确定两个对象是否逻辑相等。默认实现是比较对象的内存地址。开发者可以根据需要重写此方法,以实现自定义的相等性逻辑。
-
hashCode()方法:返回一个整数哈希值,这个值在对象的生命周期中应保持不变,且对于逻辑上相等的对象,必须返回相同的哈希值。哈希值主要用于快速定位基于哈希的集合中的对象。
如果重写了
equals()
方法,那么通常也需要重写hashCode()
方法,以确保在哈希集合中对象的正确存储和检索。这是因为哈希集合依赖于哈希码来快速访问对象,如果两个逻辑上相等的对象具有不同的哈希码,它们将被存储在不同的哈希桶中,这可能导致集合的行为异常
重写hashCode()方法的基本规则
-
在相同的应用程序执行过程中,对于同一个对象多次调用hashCode()必须返回相同的值。
-
如果两个对象根据equals()方法相等,则它们的hashCode()值必须相等。
-
但是如果两个对象equals()不相等,则它们的hashCode()值不必不同,但不同的hashCode()值可以提高哈希表的性能。
HashMap的底层数据结构
JDK 1.8后采用数组 + 链表 + 红黑树
在JDK 1.8中,HashMap 结合了数组和链表来管理哈希冲突,采用"拉链法"在数组索引处使用链表。当链表长度超过阈值(默认8)且数组长度超过64时,会转为红黑树以提升搜索效率。此改进相较于JDK 1.8之前的仅使用数组和链表,显著提高了面对大量哈希冲突时的性能。红黑树的O(log n)查找效率优于链表的O(n)。
转换决策基于链表长度和数组大小:若链表长度超标但数组不足64,会扩容数组而非转换为红黑树,以避免小数组中红黑树操作的低效。当两者条件满足时,treeifyBin 方法触发链表向红黑树的转换,从而在大规模数据和冲突中保持 HashMap 的高性能。
负载因子与扩容机制
HashMap中的扩容是基于负载因子(load factor)来决定的。
默认情况下,HashMap的负载因子为0.75,这意味着当HashMap的已存储元素数量超过当前容量的75%时,就会触发扩容操作。
例如,初始容量为16,负载洇子为0.75,则扩容阈值为16x0.75=12。当存入第13个元素时,HashMap就会触发扩容。当触发扩容时,HashMap的容量会扩大为当前容量的两倍。例如,容量从16增加到32,从32增加到64等。
扩容时,HashMap需要重新计算所有元素的哈希值,并将它们重新分配到新的哈希桶中,这个过程称为rehashing。每个元素的存储位置会根据新容量的大小重新计算哈希值,并移动到新的数组中。
为什么默认负载因子是0.75
HashMap 的默认负载因子设定为0.75,这一设计旨在平衡时间复杂度和空间复杂度。
当负载因子保持在0.75时,HashMap 能够在避免频繁扩容和减少哈希冲突之间找到一个折中点,这样的设定确保了数据结构在保持较低存储成本的同时,也优化了查找和插入操作的效率,从而维持整体性能。
在面对特定业务需求时,对 HashMap 的负载因子进行调整能够实现性能优化。例如,在高并发读取的场景中,通过降低负载因子至0.5,可以显著减少哈希冲突,进而提升读取效率。相反,在内存资源受限的环境中,提高负载因子至0.85或更高,虽然可能会对写入和查询性能造成一定影响,但能够有效减少哈希表的扩容次数,降低内存消耗。
链表和红黑树的转换时机
在Java的 HashMap 实现中,链表和红黑树的转换时机是为了保证哈希表的性能和效率。具体转换规则如下:
-
链表转换为红黑树:当单个桶中的链表长度达到8,并且 HashMap 的容量(桶数组大小)大于等于64时,链表会转换为红黑树。这种转换的目的是为了提高查找、插入和删除操作的性能,因为红黑树的这些操作的时间复杂度为O(log n),而链表的为O(n)。
-
红黑树转换回链表:当红黑树的节点数量少于等于6时,会将红黑树重新转化为链表,这是为了避免在少量节点的情况下,红黑树的平衡操作(如旋转和变色)带来的性能开销。
这种机制使得 HashMap 能够在不同的数据规模和负载条件下保持高效的性能。
为什么链表转红黑树需要数组大于等于64
-
减少不必要的树化开销:当数组容量较小时,即使发生哈希冲突,通过扩容
HashMap
也能有效地降低冲突。如果在小数组上过早地将链表转换为红黑树,一旦发生扩容,之前为树化所付出的努力可能会变得不必要,因为扩容后冲突自然减少。 -
节省内存:红黑树相较于链表需要更多的内存,特别是在节点数量较少的情况下。红黑树的额外指针和结构会占用更多的内存空间。为了在小规模数据集中避免不必要的内存消耗,
HashMap
设计为仅在数组容量达到一定规模(至少64)后才允许链表转为红黑树,这样可以在保证性能的同时,有效控制内存使用。
换句话说就是减少在小容量数组中因扩容而引发的不必要树化开销,并在节点较少时节省内存,以此在性能和内存使用之间实现平衡。
HashMap源码解析
HashMap的存储流程总览
我们先来看一下HashMap的存储流程,然后再看具体的源码:
(注:本图来源于黑马程序员,画的真的很清楚)
HashMap的常量
1.默认的负载因子,默认值是0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
2.集合最大容量
//集合最大容量的上