HashMap源码剖析
JDK7
在jdk7中,HashMap由数组+链表实现
- 数组初始大小16 阈值12(0.75*16)
- 达到阈值之后,两倍扩容
put方法
第一次put会先创建数组
如果key是null,调用putForNullKey
- 先取出数组第一个元素Entry
- 遍历Entry,如果在链表上存在key为null的节点,替换value并返回旧的value
- 否则将该元素添加到数组的第一个位置上
如果key不为null
- 先对key值进行二次hash运算,得到hash
- 然后根据通过位运算hash值找到数组下标i
- 找到下标之后,对数组table[i]这个链表进行遍历
- 如果链表中存在与当前插入元素相同的key值,新值覆盖旧值,并返回旧的值
- 否则添加,调用addEntry方法,之后返回null
addEntry
先判断是否进行扩容
- 元素达到阈值并且数组(也称为桶)的第一个元素不为null时才扩容
- 极端情况下如果数组大小等于阈值并且第一个元素是null就不会扩容
然后调用createEntry方法插入元素,把新元素作为链表的头,并赋值到数组上
- 值得注意的是,这是一个头插法,设计者认为新插入的元素更容易被访问到
- 头插法指的是,先找到链表,取出链表EntryOld,然后创建一个新的链表EntryNew,然后把EntryNew的next域指向EntryOld
扩容的实现
- 先resize()传入两倍数组的长度,
- 创建两倍大小的新数组
- 通过transfer遍历原来的数组,为每个链表元素找到新数组中的索引,调用头插法把所有的元素的引用存到新的数组
- 然后把新的数组赋给原来的数组table
- 然后重新设置阈值threshold
- 然后通过indexfor找到插入元素在扩容新数组的索引
- 之后调用createEntry方法插入元素
jdk7 put小结
get方法
如果key为null,调用getForNullKey并返回
- 如果数组长度为0,返回null
- 遍历数组中的第一个元素,对这个元素(链表)遍历,如果找到了key==null的就返回该元素的value
- 如果没找到,返回null
如果key不为null,则调用getEntry方法找到元素
- 先算出key的hash
- 然后根据hash值找到数组索引index
- 然后遍历数组对应索引下的链表table[index]
面试常问
- 数据结构? -> 数组+链表
- 如何添加数据的? -> 头插法
- 怎样预防和解决哈希冲突的?
- 预防:二次哈希 or 扰动函数
- 解决:通过其数据结构来解决->拉链法
- 默认容量? -> 16
- 内部数组什么时候创建的? -> 第一次put的时候创建的
JDK8
构造函数
- 数组初始大小:16
- 加载因子:0.75
put
内部调用了putVal方法
首次put
- 1.首次put,先调用resize()方法,给数组初始化,初始化阈值
- 2.然后判断元素放置在数组中位置i的tab[i]是否为null,如果null调用newNode创建一个新的node
- 位置i是通过与运算计算出来的,与jdk7一样
- 3.然后判断是否需要扩容,这与jdk7不同
- jdk7是先赋值后创建
- jdk8是先创建后赋值
整体流程如下
之后的put
不需要初始化数组等操作
- 4.如果元素放置在数组中位置i的tab[i](也就是当前链表的头节点)是为null,则调用newNode创建一个新的node,与第一次类似
- 5.如果当前元素放置在的位置tab[i]不为null,进入else代码块,else代码块会先创建局部变量e和k,e表示
- 5.1 如果链表元素p(也就是tab[i],也是当前链表的头节点)与新插入的元素key相同,会直接替换掉原来的元素
- 5.2 判断p是不是树节点,如果是树节点,调用树节点的putTreeVal方法
- 5.3 否则就不是树节点,说明仍然是一个链表节点,
- a.由于头节点在5.1步骤中已经处理过,所以直接遍历这个链表的后续节点
- b.每一次遍历完成之后,都对e(局部变量)重新赋值
- c.当遍历完成也就是p.next为null时,创建一个新的节点,插入到链表尾部
- d.如果当前链表长度大于8,调用treeifyBin进行树化操作,break跳出
- e.如果p.next不为空,进入下一个if,如果后续节点中找到了相同的key,直接覆盖
- 5.4 如果e不为null,即替换了旧的元素,由于e的value还是旧的值,就将新的value赋给e
put小结
treeifyBin树化操作
- 先判断数组长度是否小于64,小于64扩容,大于等于64才树化
- 先找到当前链表,然后遍历,dowhile循环将单链表转换成双向链表
- 如果头节点不为null,调用treeify进行树化操作
- treeify会遍历双向链表
- 首先将双向链表的头节点作为红黑树的根节点,当然,后续根节点可能会发生改变
- 然后通过不断遍历,生成红黑树(二叉平衡树的一种)
扩容机制
- 1.先判断老的数组是否超过阈值,如果超过了直接返回老的数组
- 2.如果没有达到阈值并且大于初始值16,数组长度变为原来的两倍,阈值赋值为原来的两倍
- 3.然后创建新的数组
- 4.然后对老的数组进行遍历,数组的每一个元素为头结点e
- 4.1 如果e是一个单节点(没有后继节点),就将老数组中的元素赋值到新数组对应的位置上
- 4.2 如果e是树节点,使用树节点的方法进行拆分赋值
- 4.3 如果e是链表,
- 首先通过dowhile循环内的与运算创建一个高位列表一个低位列表
- 然后将低位列表赋值给新数组的低位,高位列表赋值给新数组的高位
面试常问
- 1.7和1.8数据结构有什么不同?
- 1.8 增加了转换为红黑树
- 插入数据的方式?
- 1.7 的链表从前面插入
- 1.8 的链表从后面插入
- 扩容后存储位置的计算方式?
- 1.7 通过再次 indexFor() 找到数组位置
- 1.8 通过高低位的桶直接在链表尾部添加
- HashMap什么时候会把链表转化为红黑树?
- 链表⻓度超过 8 ,并且数组⻓度不小于 64
其他常用数据结构
- ArrayList:内部是数组结构
- LinkedList:列表结构
- TreeMap:二叉树
- HashMap:
- 数组+链表
- jdk8以后:数组+链表,达到一定程度以后会转化成为红黑树