前言
HashMap 又叫 Hash 表或散列表,是基于哈希表的 Map 接口实现。此实现提供了基于 Key-Value 映射结构数据的所有可选操作,如:增、删、改、查等。HashMap 并不保证映射顺序,特别是它不保证插入顺序恒久不变。HashMap是面试常考的知识点,对该知识点有必要进行一下系统的学习。
首先简单介绍一下和 HashMap 有亲戚关系的三个类,分别为 LinkedHashMap、TreeMap 和 Hashtable。类的继承关系如下图所示:
HashTable是继承自Dictionary类,而HashMap是继承自AbstractMap类。不过它们都实现了同时实现了map、Cloneable(可复制)、Serializable(可序列化)这三个接口。HashMap性能要好过Hashtable。
HashMap:(1)允许使用null键null值(key和value)都可以。这样的键只有一个,可以有一个或多个键所对应的值为null。(2)非线程安全 (3)HashMap默认的初始化大小为16。之后每次扩充,容量变为原来的2倍
ConCurrentHashMap:(1)不允许null键null值(key和value)都不可以。(2)线程安全
HashTable:(1)不允许null键null值 (2)线程安全 (3)Hashtable默认的初始大小为11,之后每次扩充,容量变为原来的2n+1
LinkedHashMap: (1) LinkedHashMap 是 HashMap 的一个子类 (2) 保存了记录的插入顺序,在用 Iterator 遍历时,先得到的记录肯定是先插入的。
TreeMap: (1) 实现了 SortedMap 接口,当用 Iterator 遍历时,得到的记录是默认按键值升序排序的。(2) 底层是红黑树
HashMap常用方法
网上找了一篇常用方法 不做过多介绍:https://blog.csdn.net/lzx_cherry/article/details/98947819
HashMap源码解析
网上找了一篇源码解析写的很详细 不做过多介绍 :https://www.jianshu.com/p/003256ce41ce
HashMap存储结构
1.Hashmap 概念理解
变量 | 术语 | 说明 |
size | 大小 | HashMap的存储大小 |
threshold | 临界值 | HashMap大小达到临界值,需要重新分配大小。 |
loadFactor | 负载因子 | HashMap大小负载因子,默认为75%。 |
modCount | 统一修改 | HashMap被修改或者删除的次数总数。 |
Entry | 实体 | HashMap存储对象的实际实体,由Key,value,hash,next组成。 |
2.存储结构
JDK 1.8 对 HashMap 进行了比较大的优化,底层实现由之前的 “数组+链表” 改为 “数组+链表+红黑树”, 当链表长度超过阈值8时,将链表转换为红黑树。当数组的元素个数大于 容量*扩容因子时,会进行扩容操作。对于 HashMap 及其子类而言,它们采用 Hash 算法(将任意长度的二进制值映射为较短的固定长度的二进制值机哈希值)来决定集合中元素的存储位置。当系统开始初始化 HashMap 时,系统会创建一个长度为 capacity (默认的容量是16)的 Entry 数组,这个数组里可以存储元素的位置被称为“桶(bucket)”,每个 bucket 都有其指定索引,系统可以根据其索引快速访问该 bucket 里存储的元素(Entry对象: key-value形式)。结构如图:
HashMap的存储结构是由数组和链表共同完成的,HashMap是Y轴方向是数组,X轴方向就是链表的存储方式。(数组的存储方式在内存的地址是连续的,大小固定,一旦分配不能被其他引用占用。它的特点是查询快,插入和删除的操作比较慢。链表的存储方式是非连续的,大小不固定,特点与数组相反,插入和删除快,查询速度慢。HashMap可以说是一种折中的方案)。
3.HashMap基本原理
1)对于 HashMap 及其子类而言,它们采用 Hash 算法来决定集合中元素的存储位置,首先判断Key是否为Null,如果为null,直接查找Enrty[0],如果不是Null,先计算Key的HashCode,然后经过二次Hash。得到Hash值,这里的Hash特征值是一个int值。
2)根据Hash值,调用indexFor 方法获取索引。indexFor方法其实主要是将hashcode换成链表数组中的下标即找到存放的桶。
3)通过索引找到bucket(hash桶),就是找到了所在的链表,然后按照链表的操作对Value进行插入、删除和查询操作。
// HashMap In Java 7
// hash :该方法主要是将Object转换成一个整型。
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
// indexFor :该方法主要是将hash生成的整型转换成链表数组中的下标。
static int indexFor(int h, int length) {
return h & (length-1);
}
4.HashMap 初始化过程
当系统开始初始化 HashMap 时,系统会创建一个长度为 capacity (默认的容量是16)的 Entry 数组,这个数组里可以存储元素的位置被称为“桶(bucket)”,每个 bucket 都有其指定索引,系统可以根据其索引快速访问该 bucket 里存储的元素(Entry对象: key-value形式)。
无论何时,HashMap 的每个“桶”只存储一个元素(也就是一个 Entry),由于 Entry 对象可以包含一个引用变量(就是 Entry 构造器的的最后一个参数)用于指向下一个 Entry,因此可能出现的情况是:HashMap 的 bucket 中只有一个 Entry,但这个 Entry 指向另一个 Entry ——这就形成了一个 Entry 链
注1:当两个key通过hashCode计算相同时,则发生了hash冲突(碰撞),HashMap解决hash冲突的方式是用链表。当发生hash冲突时,则将存放在数组中的Entry设置为新值的next(这里要注意的是,比如A和B都hash后都映射到下标i中,之前已经有A了,当map.put(B)时,将B放到下标i中,A则为B的next,所以新值存放在数组中,旧值在新值的链表上)
在Java 8中,如果一个bucket中碰撞冲突的元素超过某个限制(默认是8),则使用红黑树来替换链表,从而提高速度。这就是JDK7与JDK8中HashMap实现的最大区别。
注2:哈希碰撞的意思是计算得到的Hash值相同,需要放到同一个bucket中。Hashmap里面的bucket出现了单链表的形式,散列表要解决的一个问题就是散列值的冲突问题,通常是两种方法:链表法和开放地址法。减小碰撞方式:1.扩大容量2.优化hash算法。
链表法就是将相同hash值的对象组织成一个链表放在hash值对应的槽位;
开放地址法是通过一个探测算法,当某个槽位已经被占据的情况下继续查找下一个可以使用的槽位。
见详解:https://www.jianshu.com/p/379680144004
5.讲一下 HashMap 中 put 方法过程
1)对key的hashCode做hash操作,然后再计算在bucket中的index(1.5 HashMap的哈希函数);
2)如果没碰撞直接放到bucket里;
3)如果碰撞了,说明两个 Entry的 key的 hashCode()返回值相同,那它们的存储位置相同。此时分两种情况:
3.1首先判断这两个 Entry的 key通过equals比较返回 true,新添加 Entry的 value将覆盖集合中原有 Entry的 value,但key不会覆盖(保证key的唯一性) 。
3.2如果这两个 Entry的 key通过equals比较返回 false,新添加的 Entry将与集合中原有 Entry形成 Entry链,而且新添加的 Entry位于 Entry链的头部。
4)如果bucket满了(超过阈值,阈值=loadfactor*current capacity,load factor默认0.75),就要resize。
注1:HashMap里面没有出现hash冲突时,没有形成单链表时,hashmap查找元素很快,get()方法能够直接定位到元素,但是出现单链表后,单个bucket 里存储的不是一个 Entry,而是一个 Entry 链,系统只能必须按顺序遍历每个 Entry,直到找到想搜索的 Entry 为止——如果恰好要搜索的 Entry 位于该 Entry 链的最末端(该 Entry 是最早放入该 bucket 中),那系统必须循环到最后才能找到该元素。通过上面可知如果多个hashCode()的值落到同一个桶内的时候,这些值是存储到一个链表中的。最坏的情况下,所有的key都映射到同一个桶中,这样hashmap就退化成了一个链表——查找时间从O(1)到O(n)
注2:扩容。这个过程也叫作rehashing,因为它重建内部数据结构,并调用hash方法找到新的bucket位置。大致分两步:
1.扩容:容量扩充为原来的两倍(2 * table.length);
2.移动:对每个节点重新计算哈希值,重新计算每个元素在数组中的位置,将原来的元素移动到新的哈希表中。
6.讲一下get()方法的工作原理
通过对key的hashCode()进行hashing,并计算下标( n-1 & hash),从而获得buckets的位置。如果产生碰撞,则利用key.equals()方法去链表中查找对应的节点。
常见问题
1.能否让HashMap同步?
HashMap可以通过下面的语句进行同步:Map m = Collections.synchronizeMap(hashMap);
2.HashMap负载因子能不能设置成1?
https://mp.weixin.qq.com/s/kbLASf0lcF4PDJ3qBsFyUg
3.HashMap初始化默认为啥容量为16?
HashMap作为一种数据结构,元素在put的过程中需要进行hash运算,目的是计算出该元素存放在hashMap中的具体位置。hash运算的过程其实就是对目标元素的Key进行hashcode,再对Map的容量进行取模,而JDK 的工程师为了提升取模的效率,使用位运算代替了取模运算,这就要求Map的容量一定得是2的幂。而作为默认容量,太大和太小都不合适,所以16就作为一个比较合适的经验值被采用了。为了保证任何情况下Map的容量都是2的幂,HashMap在两个地方都做了限制。首先是,如果用户制定了初始容量,那么HashMap会计算出比该数大的第一个2的幂作为初始容量。另外,在扩容的时候,也是进行成倍的扩容,即4变成8,8变成16。详见:hashmap容量