个人全部文章同步更新与Github--Java-Notes,想了解JVM,HashMap源码分析,spring相关,剑指offer题解(Java版),能够点个star。能够看个人github主页,天天都在更新哟。java
邀请您跟我一同完成 reponode
HashMap 数据结构很是重要,常常被用来面试。由于它综合了数组以及链表的知识,还有很是重要的hash算法,在之后的工做中也常常被用到,其中还有不少很是高效的算法。可是hashMap对于不少人来讲比较困难,可能会用,可是并不清楚怎么实现,或者不清楚他的执行逻辑。git
我就经过语句的执行以及函数的调用顺序来一步步揭开 hashMap的面纱,跟着个人思路走,至少hashMap的基本逻辑就知道了,校招相关的面试基本也能答得上来github
注释应该很是很是细了,由于我基本判断语句以及一些不清楚的变量逻辑都进行了中文注释面试
Node
命名,可是我使用的是 Entry
,不过逻辑仍是1.8的逻辑存放在个人 github 上:算法
github.com/leosanqing/…spring
相似于这种格式数组
固然是为了快,为了效率数据结构
数组在知道下标以后查询速度尤为快,O(1)的时间复杂度函数
链表在增删的时候速度很是快,找到位置后(前提),处理只须要O(1)的时间复杂度,由于不须要移动数据的位置,只须要更改指向的地址便可。可是链表在遍历对比的时候很是慢,时间复杂度为O(n),因此用来作 哈希冲突时的解决方法
因此查询一个数据的时间复杂度为 O(1)+O(n)。不过由于哈希算法的很是巧妙,会让冲突尽量地均匀分布,因此链通常极其短。因此后面遍历链表的时间能够忽略不计,并且在 JDK8 以后,若是冲突的链表长度大于 8,那么就会转化为 红黑树,他的遍历的时间复杂度为O(log n)
数组的话,源码中使用的是 table
命名,你也能够称之为 桶
Node[] table;
复制代码
链表的话,JDK 1.7中使用的是 Entry
,JDK1.8采用的是 Node
命名。基本同样,只是名字不一样,结构定义以下.
(我是按照1.7的命名, 不过其余逻辑是1.8的)
/** * Entry 类 为map中基本的单元 * * key 为键,value 为值 * next 是在哈希冲突时,指向的下一个 Entry * h 为传入的hash值,源码中为 hash */
static class Entry{
Object key;
Object value;
Entry next;
int h;
}
复制代码
// 初始默认的数组容量
static final int INIT_CAPACITY = 1<<4;
//数组最大的容量,由于 数组设置为 2的整次方倍,而 32 次方为负数,因此最大只能为 1 << 30,即2的31次方
static final int MAX_CAPACITY = 1<<30;
// 默认的装填因子
static final float DEFAULT_LOADFACTOR = 0.75f;
// table 桶中的个数--数组的大小;
int size;
// 修改次数
int modCount;
// 扩容的阈值, capacity * load factor
int threshold;
// 装填因子
float loadFactor;
复制代码
若是你看懂了这个过程,那么基本上 HashMap 的主要逻辑就算是基本理解了
table[0]
的位置,若是不为空,通过运算肯定其在table
中的下标key
值是否相等,相等的话,直接覆盖value
,不相等的话遍历链表(红黑树),并插入到链表最后稍微详细些的步骤看下方思惟导图,一样缩进的为 if-else 关系
还有的细节没有写,待会儿跟着源码再细讲,我就跟着源码的调用顺序分析
import java.util.HashMap;
public class Test {
public static void main(String[] args) {
HashMap hashMap = new HashMap();
hashMap.put("name","zhangSan");
}
}
复制代码
public MyHashMap(int initCapacity,float loadFactor) {
if(initCapacity<0)
throw new IllegalArgumentException("初始化容量失败: "+
initCapacity);
if(initCapacity>= MAX_CAPACITY)
initCapacity= MAX_CAPACITY;
if(loadFactor<=0||Float.isNaN(loadFactor))
throw new IllegalArgumentException("装填因子不合法"+
loadFactor);
this.loadFactor=loadFactor;
this.threshold=tableSizeFor(initCapacity);
}
public MyHashMap(int initCapacity) {
this(initCapacity,DEFAULT_LOADFACTOR);
}
/** * 无参的,所有默认 */
public MyHashMap() {
this.loadFactor=DEFAULT_LOADFACTOR;
}
public MyHashMap(Map m){
this.loadFactor=DEFAULT_LOADFACTOR;
}
复制代码
若是没有传入参数,他就会调用无参的构造器,那么默认的长度为 16,DEFAULT_INITIAL_CAPACITY
,默认的装填因子为 0.75,DEFAULT_LOAD_FACTOR
,传入范围(0,1];
注意:这个时候,数组尚未初始化,仅仅是定义了一个Entry类型的数组
执行hashMap.put("name","zhangSan")
首先他在源码中是这样的,他又调用了putVal
函数,专门存入元素的函数(ps:源码 611行)
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
复制代码
他传入了5个值,可是咱们先重点关注前三个值,第一个是要存入的key的hash
值,第二个是key,第三个是value,至于K,V泛型若是不了解,你能够理解为 Object类型,若是按照测试的语句,你就能够把它当成 String
类型。
这个put函数,他有返回值,返回值是null,或者oldValue,看了下面的putValue
函数你就知道了
传入这个参数是为了建立节点node以及计算索引时用
源码(第337 行)
static final int hash(Object key) {
int h;
// 将key 的高16位和低16位进行异或
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
复制代码
这个也是 JDK 1.8的改进,1.7不是这样的。
主要是从速度、功效、质量来考虑的,这么作能够在数组table的n比较小的时候,也能保证考虑到高低bit都参与到hash的计算中(为了是分布更均匀),同时不会有太大的开销。
private Object putVal(int hash, Object key, Object value, boolean onlyIfAbsent, boolean evict) {
Entry[] tab;
Entry p;
int n,i;
// 若是第一次 进行存放数据,进行初始化,table 被延迟到进行数据存放时才初始化
if((tab = table) == null || (n = table.length)==0){
n = (tab = resize()).length;
}
if((p = table[i = ((n - 1) & hash)]) == null){
tab[i] = newEntry(hash,key,value,null);
}
else {
Entry e;
Object k;
// 若是 key 相同,那么就直接将 value 覆盖
// 为何要比较这么屡次
// 1.首先判断 哈希值是否相同
if(p.h == hash &&
// 2.判断两个key是否相等,使用 '==' 是非字符串状况,之比较两个的内容,使用'equals' 是针对字符串
(((k = p.key) == key) || (key != null && key.equals(k))))
// 覆盖value值
e = p;
// 这个是树的状况
//else if(p instance of TreeNode)
// 链
else{
for(int binCount=0;;++binCount){
// 遍历到最后,插入
if((e = p.next) == null){
p.next = newEntry(hash,key,value,null);
/* 若是 binCount >=转化树的阈值-1 ,则将链表转化为树 if(binCount >= TREEIFY_THRESHOLD-1) treeifyBin(tab,hash); */
break;
}
if(p.h == hash &&
(((k = p.key) == key) || (key != null && key.equals(k))))
break;
// 移动到下一个
p = e;
}
// 若是有相应的映射,即key相同
if(e != null){
Object oldValue = e.value;
if(!onlyIfAbsent || oldValue == null)
e.value = value;
return oldValue;
}
}
}
// 修改次数 ++
++ modCount;
// 大于阈值就扩容
if(++size >threshold)
resize();
//afterNodeInsertion(evict);
return null;
}
复制代码
看了上面的源码分析你就能解决上面的疑问,put函数有返回值,返回值为null
或者oldValue
。
先记住答案:当他不产生覆盖的时候,返回null;当他产生覆盖的时候返回 oldVal,即原来被覆盖的值
咱们先进行测试,你就大概知道意思了
import java.util.HashMap;
public class Test {
public static void main(String[] args) {
HashMap hashMap = new HashMap();
hashMap.put("name","张三");
Object oldValue1 = hashMap.put("name","李四");
Object oldValue2 = hashMap.put("age",18);
System.out.println("oldValue = " + oldValue1);
System.out.println("oldValue2 = " + oldValue2);
}
}
复制代码
我想如今你应该清楚了,当输入的key的内容相同,hash值也相同的时候,他就会覆盖以前的Value值,而且返回被覆盖前的value值。(假设输入的只是String类型,若是是自定义的对象,须要重写 hashCode 和 equals 方法)
这个的关键代码在上面函数的++modCount
一行上面,我有注释
//若是有相应的映射,即key相同
if(e != null){
Object oldValue = e.value;
if(!onlyIfAbsent || oldValue == null)
e.value = value;
return oldValue;
}
复制代码
首先要判断table数组是否初始化了,即这条语句if ((tab = table) == null || (n = tab.length) == 0)
,
resize
方法(后面分析).能够直接看索引为 resize函数
的内容将 key 的 hash 值和table.length-1
相与,相与的结果就是要存入的元素的table中的 位置tab[(n - 1) & hash]
。
这个时候看源码,它分为两种状况:
第一种:相应的索引上没有元素(只有这个时候 size才++,相应索引上有元素,size是不会 ++ 的)
// 若是table 数组的相应的索引上没有元素,那么直接建立一个新的节点
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 修改次数++
++modCount;
// 判断是否须要扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
复制代码
如今知道啥时候返回 null了吧
第二种:相应的索引上有元素
这个时候就要判断元素的key是否相等
if(p.h == hash &&(((k = p.key) == key) || (key != null && key.equals(k))))
else {
Entry e;
Object k;
// 若是 key 相同,那么就直接将 value 覆盖
// 为何要比较这么屡次
// 1.首先判断 哈希值是否相同
if(p.h == hash &&
// 2.判断两个key是否相等,使用 '==' 是非字符串状况,之比较两个的内容,使用'equals' 是针对字符串
(((k = p.key) == key) || (key != null && key.equals(k))))
// 覆盖value值
e = p;
// 这个是树的状况
//else if(p instance of TreeNode)
// 链
else{
for(int binCount=0;;++binCount){
// 遍历到最后,插入
if((e = p.next) == null){
p.next = newEntry(hash,key,value,null);
/* 若是 binCount > 转化树的阈值 ,则将链表转化为树 if(binCount >= TREEIFY_THRESHOLD-1) treeifyBin(tab,hash); */
break;
}
if(p.h == hash &&
(((k = p.key) == key) || (key != null && key.equals(k))))
break;
// 移动到下一个
p = e;
}
// 若是有相应的映射,即
if(e != null){
Object oldValue = e.value;
if(!onlyIfAbsent || oldValue == null)
e.value = value;
return oldValue;
}
}
}
复制代码
这就是返回 oldValue的状况,固然上面的也有状况并不会返回oldValue
这个是进行扩容的函数,也是很是重要的,要确保每次扩容先后容量大小都是2的n次方
。而且在JDK 1.8中,对这个函数进行了优化,使得算法很是的高效
初始化 数组table。在putVal函数中,(源码第628行)
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
复制代码
进行扩容数组table的size达到阈值时,即++size > load factor * capacity 时,也是在putVal
函数中
if (++size > threshold)
resize();
复制代码
忽略了树的逻辑,只有相应的条件
final Entry[] resize() {
// 定义旧的数组为 Entry 类型的数组,oldTab
Entry[] oldTab = table;
// 若是oldTab==null 则返回 0,不然返回数组大小
int oldCap = (oldTab==null) ? 0 : oldTab.length;
int oldThreshold = threshold;
int newCap=0,newThreshold=0;
// 说明已经不是第一次 扩容,那么已经初始化过,容量必定是 2的n次方,因此能够直接位运算
if(oldCap>0){
// 若是 原来的数组大小已经大于等于了最大值,那么阈值设置为 Integer的最大值,即不会再进行扩容
if(oldCap >= MAX_CAPACITY){
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 所以已经不是第一次扩容,必定是2的n次方
else if ((newCap = oldCap << 1) < MAX_CAPACITY &&
oldCap >= INIT_CAPACITY)
newThreshold = oldThreshold << 1;
}
// 若是oldThreshold > 0,而且oldCap == 0,说明是尚未进行调用resize方法。
// 说明输入了初始值,且oldThreshold为 比输入值大的最小的2的n次方
// 那么就把 oldThreshold 的值赋给 newCap ,由于这个值如今为 比输入值大的最小的2的n次方
else if(oldThreshold>0)
newCap = oldThreshold;
// 知足这个条件只有调用无参构造函数,注意只有;
else{
newCap = INIT_CAPACITY;
newThreshold = (int) (INIT_CAPACITY * DEFAULT_LOADFACTOR);
}
if(newThreshold == 0){
float ft = (float) (newCap * loadFactor);
newThreshold =(newCap < MAX_CAPACITY && ft < (float) MAX_CAPACITY ?
(int )ft : Integer.MAX_VALUE);
}
threshold = newThreshold;
Entry newTable[] = new Entry[newCap];
table=newTable;
// 将原来数组中的全部元素都 copy进新的数组
if(oldTab != null){
for (int j = 0; j < oldCap; j++) {
Entry e;
if((e = oldTab[j]) != null){
oldTab[j] = null;
// 说明尚未成链,数组上只有一个
if(e.next == null){
// 从新计算 数组索引 值
newTable[e.h & (newCap-1)] = e;
}
// 判断是否为树结构
//else if (e instanceof TreeNode)
// 若是不是树,只是链表,即长度尚未大于 8 进化成树
else{
// 扩容后,若是元素的 index 仍是原来的。就使用这个lo前缀的
Entry loHead=null, loTail =null;
// 扩容后 元素index改变,那么就使用 hi前缀开头的
Entry hiHead = null, hiTail = null;
Entry next;
do {
next = e.next;
if((e.h & oldCap) == 0){
// 若是 loTail == null ,说明这个 位置上是第一次添加,没有哈希冲突
if(loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else{
if(hiTail == null)
loHead = e;
else
hiTail.next = e;
hiTail = e ;
}
}while ((e = next) != null);
if(loTail != null){
loTail.next = null;
newTable[j] = loHead;
}
// 新的index 等于原来的 index+oldCap
else {
hiTail.next = null;
newTable[j+oldCap] = hiHead;
}
}
}
}
}
return newTable;
}
复制代码
// 将原来数组中的全部元素都 copy进新的数组
if(oldTab != null){
for (int j = 0; j < oldCap; j++) {
Entry e;
if((e = oldTab[j]) != null){
oldTab[j] = null;
// 说明尚未成链,数组上只有一个
if(e.next == null){
// 从新计算 数组索引 值
newTable[e.h & (newCap-1)] = e;
}
// 判断是否为树结构
//else if (e instanceof TreeNode)
// 若是不是树,只是链表,即长度尚未大于 8 进化成树
else{
// 扩容后,若是元素的 index 仍是原来的。就使用这个lo前缀的
Entry loHead=null, loTail =null;
// 扩容后 元素index改变,那么就使用 hi前缀开头的
Entry hiHead = null, hiTail = null;
Entry next;
do {
next = e.next;
//这个很是重要,也比较难懂,将它和原来的长度进行相与,就是判断他的原来的hash的上一个 bit 位是否为 1.下面我再详细说
if((e.h & oldCap) == 0){
// 若是 loTail == null ,说明这个 位置上是第一次添加,没有哈希冲突
if(loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else{
if(hiTail == null)
loHead = e;
else
hiTail.next = e;
hiTail = e ;
}
}while ((e = next) != null);
if(loTail != null){
loTail.next = null;
newTable[j] = loHead;
}
// 新的index 等于原来的 index+oldCap
else {
hiTail.next = null;
newTable[j+oldCap] = hiHead;
}
}
}
}
}
复制代码
从上面的代码能够看出来,他遍历数组。将每一个元素和原来的数组长度进行与运算,判断是否为 0
若是为0,那么索引位置不变,
若是不为 0,那么索引位置等于 原来的索引+原来的数组长度,
你可能有点纳闷,为啥要这样,请参考下这篇文章。
不过阅读前,我以为得了解这些前提,
知道这个前提,那么你就知道在数组的长度中,只有最高位是1,其余全为0;
文章连接:www.jianshu.com/p/4177dc15d…
上面的这个算法很是重要,也是JDK1.8以后的优化,效率很是高
至此,put一个元素的过程基本就完了,可能还有一些小细节没讲到(应该不过重要,能够自行查看个人注释)
若是你put
方法搞懂了,那么后面的get,contains,remove,iterator 这些基本没有啥大的障碍,这些搞懂,hashMap的 70% 至少都懂了
后面应该还有上述方法的源码分析以及回答一些疑问。
好比"为啥hashMap的数组长度必定是2的n次方",
"当我new HashMap()的时候,输入的初始容量 0,1,2,3,4,5,6。table初始化的值到底为多少"
等等