HashMap扩容机制,HashMap的底层原理以及HashSet在底层原理
HashMap 扩容机制
HashMap
是 Java 中一个非常常用的键值对集合,它基于哈希表(Hash Table)实现,具有很高的查找、插入效率。HashMap
的扩容机制主要包括以下几个关键点:
- 初始容量(Initial Capacity):
- 默认初始容量为 16,但可以通过构造函数设置。
- 构造函数:
new HashMap<>(initialCapacity)
,其中initialCapacity
是初始容量。
- 负载因子(Load Factor):
- 负载因子决定了 HashMap 的何时扩容。默认负载因子为 0.75,表示当 HashMap 中的元素个数超过
容量 * 负载因子
时,就会触发扩容。 - 例如,初始容量为 16,负载因子为 0.75,那么当元素个数超过 12(16 * 0.75)时,HashMap 会扩容。
- 负载因子决定了 HashMap 的何时扩容。默认负载因子为 0.75,表示当 HashMap 中的元素个数超过
- 扩容机制:
- 扩容时,
HashMap
会将容量扩大为原来的 2 倍,当HashMap
扩容时,并不会直接修改原有的数组,而是创建一个 新的更大的数组,并重新计算所有元素的哈希值并重新分配到新的数组(或者桶)中。 - 这意味着,扩容后,HashMap 的容量将变为
容量 * 2
。 - 扩容的代价是 O(n),因为需要重新计算每个元素的哈希值并将它们重新放入新的数组中。
- 扩容时,
- 扩容触发条件:
- 当元素个数大于
容量 * 负载因子
时触发扩容。 - 举例来说,如果初始容量为 16,负载因子为 0.75,当元素个数超过 12 时触发扩容(16 * 0.75 = 12)。
- 当元素个数大于
- 扩容时的性能问题:
- 由于扩容时需要将所有元素重新计算哈希位置并放入新的数组,因此扩容是一个较为耗时的操作,尤其是当元素非常多时。
- 为了避免频繁扩容,通常可以根据预期元素数量来调整初始容量。
为什么需要创建新的数组?
- 空间扩展:原数组在达到一定容量后,如果继续使用,就会导致哈希冲突频繁发生,影响性能。通过创建新的数组,
HashMap
可以提供更多的桶(bucket),降低哈希冲突的概率。- 重新分配哈希位置:数组扩容后,原来计算位置的方式已经不适用了,因为数组的大小变了。所以必须重新计算每个元素的哈希位置,确保数据均匀分布。
HashMap 的底层原理
-
创建一个默认长度16,默认加载因子为0.75的数组,数组名table
16*0.75 = 12,如果存入的数据达到12,则数组自动扩容为原来的2倍
-
然后根据put()方法进行添加元素,在put()方法的底层会创建一个Entry对象 ,Entry对象里面记录的是要存放的键和值 ,然后利用键计算哈希值(只要键的哈希值,跟值无关),然后根据元素的哈希值跟数组的长度计算出元素应存入的索引位置
int index = (数组长度-1) & 哈希值;
-
如果位置为null,直接存入
-
如果位置不为null 表示有元素,则调用equals方法比较键的属性值
如果键里面的数据是一样则会覆盖原有的Entry对象
如果键里面的数据不一样则会添加新的Entry对象
jdk8以前:如果在数组中的同一个位置插入不同的元素的话,新元素存入数组,老元素挂在新元素下面,形成链表
jdk8以后:如果在数组中的同一个位置插入不同的元素的话,新元素直接挂在老元素下面
-
jdk8以后,当链表长度超过8,且数组长度大于等于64时,链表自动转换为红黑树。
-
如果键存储的是自定义对象,需要重写hashCode方法和equals方法
如果值存储自定义对象,不需要重写hashCode和equals方法
依赖hashCode和equals方法保证键的唯一
特点都是由键决定的:无序、不重复、无索引
HashMap
底层是基于数组和链表/红黑树(JDK 8及以后)实现的,具有以下几个重要组件和工作机制:
- 数组 + 链表/红黑树:
HashMap
内部维护了一个数组(称为桶数组),每个桶可以存储多个键值对。当发生哈希冲突时,多个元素会被存储在同一个桶中,通常通过链表(JDK 7及以前)或红黑树(JDK 8及以后)来解决冲突。
- 哈希桶(Bucket):
HashMap
使用哈希函数计算键的哈希值,然后将键值对放入相应的桶中。桶的下标是通过hash(key) % array.length
计算得到的。- 如果两个键的哈希值相同,它们会被放入同一个桶,这就是哈希冲突。
- 链表(在哈希冲突时):
- 在 JDK 7 及以前,如果两个元素的哈希值相同,它们会被放在同一个桶的链表中。
- 查找时,链表的长度决定了查找的效率,链表越长,查找的时间复杂度越高。
- 红黑树(JDK 8及以后):
- 从 JDK 8 开始,如果某个桶中的元素个数超过 8 且数组的大小超过 64,
HashMap
会将链表转换为红黑树。 - 红黑树是一种自平衡的二叉查找树,它可以保证最坏情况下的查找时间复杂度为 O(log n),大大提高了性能。
- 从 JDK 8 开始,如果某个桶中的元素个数超过 8 且数组的大小超过 64,
- 哈希函数:
- 哈希函数是
HashMap
关键的性能因素。Java 使用了hashCode()
方法和扰动函数来计算键的哈希值,从而将元素均匀地分布到不同的桶中。 HashMap
使用扰动函数(如hash ^ (hash >>> 16)
)来减少哈希冲突,确保哈希值分布更均匀。
- 哈希函数是
HashSet在底层原理
-
创建一个默认长度16,默认加载因子为0.75的数组,数组名table
16*0.75 = 12,如果存入的数据达到12,则数组自动扩容为原来的2倍
-
根据元素的哈希值跟数组的长度计算出应存入的位置
int index = (数组长度-1) & 哈希值;
-
如果位置为null,直接存入
-
如果位置不为null 表示有元素,则调用equals方法比较属性值
一样;不存 不一样:存入数组,形成链表
jdk8以前:新元素存入数组,老元素挂在新元素下面
jdk8以后:新元素直接挂在老元素下面
-
jdk8以后,当链表长度超过8,且数组长度大于等于64时,链表自动转换为红黑树。
-
如果集合中存储的时自定义对象,必须重写hashCode方法和equals方法。
HashSet是利用什么机制保证去重?
- 利用HashCode方法和equals方法
- HashCode方法来计算出哈希值,根据哈希值计算机出要元素要存入的位置,然后再调用equals方法比较对象内部的属性值是否一样