-
JDK1.8之后,HashMap的数据结构采用“数组+链表+红黑树”实现
-
当没有哈希冲突时,元素保存到数组中
-
如果哈希冲突,在对应的位置上创建链表,元素保存到链表中
-
当链表元素数量大于8,转换为红黑树
-
-
采用哈希表实现的集合
-
数据采用“键值树”的形式保存,键称为key,值称为value,键不能重复,允许null,值没有限制,键和值都是引用类型
-
在哈希表中,哈希码就是键,保存的数据就是值,可以通过键得到相应的值
构造方法
常用构造方法 | 说明 |
---|---|
HashMap() | 创建一个空的集合对象,默认大小为16,加载因子为0.75 |
常用方法
常用方法 | 作用 |
---|---|
put(Object key,Object Value) | 添加一组键值对 |
get(Object key) | 根据键得到值 |
size() | 得到键值对的数量 |
clear() | 清空所有键值对 |
keyset() | 得到所有键的集合 |
values() | 得到所有值的集合 |
containsKey(Object key) | 判断是否包含某个键 |
containsValue(Object key) | 判断是否包含某个值 |
remove(Object key) | 根据键删除键值对 |
面试题:详细说一下HashMap的底层原理
从数据结构、put()流程、扩容机制三个方面来具体回答。
数据结构
在JDK8中,HashMap底层是采用“数组+链表+红黑树”来实现的。 HashMap是基于哈希算法来确定元素的位置(槽)的,当我们向集合中存入数据时,它会计算传入的Key的哈希值,并利用哈希值取余来确定槽的位置。如果元素发生碰撞,也就是这个槽已经存在其他的元素了,则HashMap会通过链表将这些元素组织起来。如果碰撞进一步加剧,某个链表的长度达到了8,则HashMap会创建红黑树来代替这个链表,从而提高对这个槽中数据的查找的速度。
HashMap中,数组的默认初始容量为16,这个容量会以2的指数进行扩容。具体来说,当数组中的元素达到一定比例的时候HashMap就会扩容,这个比例叫做负载因子,默认为0.75。自动扩容机制,是为了保证HashMap初始时不必占据太大的内存,而在使用期间又可以实时保证有足够大的空间。采用2的指数进行扩容,是为了利用位运算,提高扩容运算的效率。
put()流程
put()方法的执行过程中,主要包含四个步骤: 1. 判断数组,若发现数组为空,则进行首次扩容。 2. 判断头节点,若发现头节点为空,则新建链表节点,存入数组。 3. 判断头节点,若发现头节点非空,则将元素插入槽内。 4. 插入元素后,判断元素的个数,若发现超过阈值则再次扩容。 其中,第3步又可以细分为如下三个小步骤: 1. 若元素的key与头节点一致,则直接覆盖头节点。 2. 若元素为树型节点,则将元素追加到树中。 3. 若元素为链表节点,则将元素追加到链表中。追加后,需要判断链表长度以决定是否转为红黑树。若链表长度达到8、数组容量未达到64,则扩容。若链表长度达到8、数组容量达到64,则转为红黑树。
扩容机制
向HashMap中添加数据时,有三个条件会触发它的扩容行为: 1. 如果数组为空,则进行首次扩容。 2. 将元素接入链表后,如果链表长度达到8,并且数组长度小于64,则扩容。 3. 添加后,如果数组中元素超过阈值,即比例超出限制(默认为0.75),则扩容。 并且,每次扩容时都是将容量翻倍,即创建一个2倍大的新数组,然后再将旧数组中的数组迁移到新数组里。由于HashMap中数组的容量为2^N,所以可以用位移运算计算新容量,效率很高。
HashMap是非线程安全的,在多线程环境下,多个线程同时触发HashMap的改变时,有可能会发生冲突。所以,在多线程环境下不建议使用HashMap,可以考虑使用Collections将HashMap转为线程安全的HashMap,更为推荐的方式则是使用ConcurrentHashMap。