HashMap的结构和原理经常会被问到,比较经典的结构存储,1.8又对之前1.7做了较大优化,所以经常放在一起对比,这里分析下1.8的实现,并且总结下和1.7之间的差别。
构造方法
//这里可以传入容量和扩容因子,默认的是16和0.75,传入值可以改变
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
// 这里的经过和1.7一致,这里确认下数组的长度,因为要取的大于等于传入容量2的n次幂
// 不过加载时机不一致,1.7直接存的传入的容量值,然后初始化数组的时候判断的需要的
// 数组的长度,这里直接计算数组长度
this.threshold = tableSizeFor(initialCapacity);
}
static final int tableSizeFor(int cap) {
// 这里计算方式不一致,不过得到的还是大于等于cap的2的n次幂
// -1 是最高位符号位为1,最低位为1,然后带符号右移动 (容量-1)的0开头的个数
// 得到的就是符合条件的2的n次幂
int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
构造方法里面基本一致,就是赋值下对应的容量和扩容因子,不过对比1.7的确认数组长度提前,计算长度算法改变了下,不影响大局
HashMap的put方法
比较关键的还是HashMap的put方法,可以说是map里面的核心,下面来看下put的实现
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h;
// hash算法,这里直接对象的hashcode然后异或下高16位
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 如果数组还没有进行初始化,调用resize进行初始化,扩容也是这个,后面要详细分析下
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 这里是对应的下标位置没有值的话直接填入,这里直接hash值&数组长度得到下标
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
// 这里就是数组正常,位置还有值
Node<K,V> e; K k;
// 这里就是数组第一个就是跟要插入值是一样的,就要记录下来这个节点到e引用,后面改变这个值
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 如果是已经树化 的节点,走树的插入流程,后面单独分析下
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 遍历,记录的就是链表的长度了
for (int binCount = 0; ; ++binCount) {
// 遍历到最后的节点,进行插入(尾插法)
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 如果binCount>=树化的阈值,这个默认是8,也就是binCount>=7
// 会进行树化,因为binCount可以认为是下标,时机实际链表数量为8
//然后新插入一个值,代表当前是第9个值
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
// 树化的方法,单独分析
treeifyBin(tab, hash);
break;
}
// 这里是找到了相同的key,这时候e指向的就是这个相同的Node
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
// 这里是链表的遍历,即引用指向下一个节点
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
// 这里是在判断是不是允许覆盖旧的值
if (!onlyIfAbsent || oldValue == null)
e.value = value;
// 留给子类处理的,未实现
afterNodeAccess(e);
// 返回旧值
return oldValue;
}
}
// 这里代表就有新增的数据,增加对应的数量
++modCount;
// 如果大于要扩容的阈值,进行扩容
if (++size > threshold)
resize();
// 未实现
afterNodeInsertion(evict);
return null;
}
这里put的流程简单总结下,如果数组未初始化先调用resize进行初始化,然后计算对应的位置,如果hash不冲突直接存入,hash冲突的情况下,去判断是链表还是树,如果是树走树的插入逻辑putTreeVal,如果是链表,那么进行遍历链表,如果有相同的key值,记录引用,后面判断是否替换值,无相同key,遍历到链表尾部进行插入,然后判断是不是数量超过8,是否需要树化treeifyBin,最后增加对应的map数据量,然后判断是不是超过指定的扩容阈值,超过的话需要扩容。
那么和1.7的区别有哪些呢,简单列下看到的差别?
- hash算法差别
- 插入方法差别,采用了尾插法
- 增加了红黑树结构,链表达到一定长度后进行树化
- 扩容机制发生了变化,1.7对应的是如果直接插入到数组,无hash冲突那么无需扩容,这里只要进行插入原本没有的数据,导致map数据量增加,超过阈值就需要扩容
resize 方法分析
resize 在1.8中承载了扩容和初始化的作用,那么里面是怎么区分的呢,又是怎么进行初始化和扩容的呢
final Node<K,V>[] resize() {
// 现在的tab作为oldTab
Node<K,V>[] oldTab = table;
// 如果是tab未初始化状态,那么oldCap就是0,否则就是tab的长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 扩容阈值,这里对于未初始化的是直接 cap的值
int oldThr = threshold;
// 定义新
int newCap, newThr = 0;
// 旧的容量大于0,说明是扩容,不是初始化的时候
if (oldCap > 0) {
// 判断是不是超过限制最大值
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 这里新的容量是左移动一位得到的2倍大小的值
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 这里是在扩容之后小于最大值,并且原来的容量大于等于默认容量的时候,
//新的扩容阈值直接也是翻倍,原有容量小于默认16的时候不成立
newThr = oldThr << 1; // double threshold
}
// 这里是初始化的时候,oldThr因为是构造的时候直接赋值的 map容量,这里赋值给新的容量
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
// 能走到这里说明hashmap给的默认容量是不大于0的,那么采用默认值,扩容阈值也直接采用默认值
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
// 新的扩容阈值为0,说明直接走的oldThr > 0 新的扩容阈值还没赋值
// 然后计算扩容阈值,判断下别超过最大值
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
// 计算得到新的扩容阈值赋值给全局的扩容阈值
threshold = newThr;
// 创建新的数组
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
// 新数组赋值给数组
table = newTab;
// 如果旧的有数据,准备转移
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
// 下标位置有值的情况
if ((e = oldTab[j]) != null) {
// 把原位置变空
oldTab[j] = null;
// 如果只有一个值,那么直接根据hash计算下标赋值到新的数组
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
// 如果是树形的结构,走split,这个再单独分析
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
// 链表的情况
// 这里记录的是低位的链表头节点和尾节点
Node<K,V> loHead = null, loTail = null;
// 这里记录高位的头尾节点,这里的高低指的的数组的前后半段
// 因为新的数组扩容为原来的二倍,原来下标的称为低位,新增的下标称为高位
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
// 先拿到下一个的引用
next = e.next;
// 跟旧的数组长度做&,因为旧的数组长度为2的n次幂,其实只有最高位为1
// 这里就是在比较hash值和数组长度对应的最高位是1还是0
if ((e.hash & oldCap) == 0) {
// 为0的时候落到前半段,即下标不变
// 这里的原因是啥呢,就是说一个hash值,落到那个位置
// 可以认为是取模,即除以数组长度得到的余数,那么得到的商呢
// 因为数组长度是扩容二倍,那么原来的hash值除以原来的数组长度
// 得到的商是奇数,那么就代表可以落在高位,如果是偶数,那么就落在
// 低位(0除外),或者可以认为偶数都是2的倍数,都可以当0计算
// 会落到低位,然后怎么判断商是奇偶呢,那么就是对应数组长度的位是0
// 还是1了,是0就是偶数倍,1就是奇数倍
if (loTail == null)
// 这里尾节点为空,证明还没初始化,直接作为头节点
loHead = e;
else
// 尾节点存在的情况下,直接插入到尾节点的后面
loTail.next = e;
// 这里把当前节点设置为尾节点
loTail = e;
}
else {
// 这里处理高位的情况,同上
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
// 这里的while在进行的当前指针的下移动
} while ((e = next) != null);
if (loTail != null) {
// 尾节点不为空,证明当前存在值,然后把下一个节点变为空,因为有可能之前的链表会链接到
// 原来高位节点的情况
loTail.next = null;
// 这里把头节点放进新的数组
newTab[j] = loHead;
}
if (hiTail != null) {
// 高位同理,下标对比增加原来数组长度
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
resize的流程暂时就是计算新的数组长度,新的扩容阈值,然后赋值到对应的变量上,然后对应的树的节点转移单独分析,链表节点的转移是直接根据hash值决定是在低位还是在高位,把原有的链表拆分为两个链表,然后插入到对应的数组位置,
和1.7 的差别,这里看起来差别还蛮大的
- 扩容同时承载了初始化和扩容
- 是直接把新的数组和扩容阈值赋值给了对应的变量,1.7是先转移数据再赋值过去的
- 转移数据不需要重新hash了
- 树状的单独转移
- 链表的转移不是一个个的进行转移了,而是先把一个链表按照hash值分为高低两个链表,然后去赋值到对应数组位置
这里再单独分析下红黑树状的是怎么进行转移数据的
红黑树的转移数据split
这里传入的值是hashmap本身,新的数组,原来的数组下标,原来的数组长度
这里是TreeNode的一个方法,TreeNode继承了LinkedHashMap的Entry,间接继承HashMap的Node
可以先简单想像为一个二叉树,然后每个节点有相邻节点的引用,然后具体一个red的标志红黑
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
TreeNode<K,V> b = this;
// Relink into lo and hi lists, preserving order
// 这里同链表转移也是区分下高位和低位
TreeNode<K,V> loHead = null, loTail = null;
TreeNode<K,V> hiHead = null, hiTail = null;
// 这里记录的是分配到高低节点的数量
int lc = 0, hc = 0;
// 遍历树节点
for (TreeNode<K,V> e = b, next; e != null; e = next) {
// 还是先拿到下一个引用
next = (TreeNode<K,V>)e.next;
// 然后断掉下一个引用
e.next = null;
// 这里做hash判断高地位,同链表
if ((e.hash & bit) == 0) {
if ((e.prev = loTail) == null)
// 未初始化的时候,直接本节点变为头节点
loHead = e;
else
loTail.next = e;
loTail = e;
// 计数
++lc;
}
else {
// 高位同理
if ((e.prev = hiTail) == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
++hc;
}
}
// 如果树的头节点不为空的话
if (loHead != null) {
// 判断下数量,如果小于需要退化成链表的阈值,进行退化,这里后面单独分析
if (lc <= UNTREEIFY_THRESHOLD)
tab[index] = loHead.untreeify(map);
else {
// 不需要退化链表的话,直接
tab[index] = loHead;
// 不为空的时候进行树化,后面单独分析
if (hiHead != null) // (else is already treeified)
loHead.treeify(tab);
}
}
// 高位同理
if (hiHead != null) {
if (hc <= UNTREEIFY_THRESHOLD)
tab[index + bit] = hiHead.untreeify(map);
else {
tab[index + bit] = hiHead;
if (loHead != null)
hiHead.treeify(tab);
}
}
}
这里的树的结构的转移很类似于链表的数据转移,不过看到的树也只用到的链表的方法,即next指针,还没维护其他的引用,那么就后面单独看下untreeify 退化为链表 和treeify 变为红黑树都做了什么操作?下面就主要分析红黑树相关的结构已经方法是用了
插播下TreeNode的基本结构
主要就是这个结构
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
}
putTreeVal 方法分析
先来看下树的putVal操作,放大接受的值就是map本身,数组,hash值,key和value
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
int h, K k, V v) {
Class<?> kc = null;
boolean searched = false;
// 这里在看parent节点不为空的时候获取下跟节点,为空的时候自己
TreeNode<K,V> root = (parent != null) ? root() : this;
for (TreeNode<K,V> p = root;;) {
int dir, ph; K pk;
// 在记录大小,遍历节点的hash值大于要插入的hash值
if ((ph = p.hash) > h)
dir = -1;
//小于的时候
else if (ph < h)
dir = 1;
// hash值相同,并且equals的时候直接返回当前节点,由调用者处理替换值的操作
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
// 这里在分析key的类型,是不是实现了排序,排序规则确定大小的,不深入分析
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
// 这里的针对的是没排出来两个值大小的,可以认为一样大
if (!searched) {
// 第一次的进来
TreeNode<K,V> q, ch;
searched = true;
// 分别在左子树和右子树进行查找,查到了就返回
// find方法不详细分析了,可以认为是树或者想象二分查找,如果分不出来大小,就左右都查
if (((ch = p.left) != null &&
(q = ch.find(h, k, kc)) != null) ||
((ch = p.right) != null &&
(q = ch.find(h, k, kc)) != null))
return q;
}
// 当hashCodes相等且不可比较时,用于排序插入的平局打破实用程序。
//我们不需要总顺序,只需要一个一致的插入规则来保持重新平衡的等价性。
// 超过必要程度的打结会稍微简化测试。
dir = tieBreakOrder(k, pk);
}
// 走到这里的就可以分辨大小的
TreeNode<K,V> xp = p;
// 这里是遍历到为null,也就是尾节点的时候
if ((p = (dir <= 0) ? p.left : p.right) == null) {
// 根据大小到左边或者到右边进行遍历
Node<K,V> xpn = xp.next;
// new的新节点
TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
// 根据大小放到左边还是右边
if (dir <= 0)
xp.left = x;
else
xp.right = x;
xp.next = x;
x.parent = x.prev = xp;
if (xpn != null)
((TreeNode<K,V>)xpn).prev = x;
// 红黑树平衡不详细展开,再追红黑树细节就是为难我胖虎
moveRootToFront(tab, balanceInsertion(root, x));
return null;
}
}
}
final TreeNode<K,V> root() {
for (TreeNode<K,V> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
简单概括下吧,就是会判断下key的hash值的大小,达到一个顺序状态,然后去进行遍历,维护了left,right,parent,next等引用,插入到对应的位置,然后重新平衡(暂不表),红黑树主要优化了链表的遍历为log2^N
treeifyBin 方法分析
树化方法的简单分析
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
// 如果数组长度小于默认值的时候触发扩容,不进行树化
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
// 遍历然后变为treenode,维护了前后节点
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
// 有值的情况下进行树化,分析下
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
treeifyBin 就是判断了下当前数组的容量,如果小于树化的最小值,那么直接触发扩容,不进行树化了,如果不小于默认值,走树化,维护下prev和next的指向,然后treeify,还是分析下这个吧
treeify�分析
final void treeify(Node<K,V>[] tab) {
TreeNode<K,V> root = null;
for (TreeNode<K,V> x = this, next; x != null; x = next) {
next = (TreeNode<K,V>)x.next;
x.left = x.right = null;
if (root == null) {
x.parent = null;
x.red = false;
// 跟节点为空,第一个就是跟节点
root = x;
}
else {
K k = x.key;
int h = x.hash;
Class<?> kc = null;
for (TreeNode<K,V> p = root;;) {
int dir, ph;
K pk = p.key;
// 判断hash的大小
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
TreeNode<K,V> xp = p;
// 这里也是根据大小来确认是放到左边还是右边,其实就是根据hash确认一个相对有序的
// 这里维护了左右节点
// 插入之后重新平衡(红黑树)
if ((p = (dir <= 0) ? p.left : p.right) == null) {
x.parent = xp;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
root = balanceInsertion(root, x);
break;
}
}
}
}
// 这里是把跟节点移动到前面,确保给定的根是其bin的第一个节点。
moveRootToFront(tab, root);
}
这里是树,但是还是维护了链表的结构,即前后节点
untreeify 方法分析
final Node<K,V> untreeify(HashMap<K,V> map) {
Node<K,V> hd = null, tl = null;
for (Node<K,V> q = this; q != null; q = q.next) {
// 直接替换为Node节点,然后维护next节点之后遍历就行了
Node<K,V> p = map.replacementNode(q, null);
if (tl == null)
hd = p;
else
tl.next = p;
tl = p;
}
return hd;
}
这里链表化比较简单,因为树里面维护了下一个节点,这样简单遍历重新组合为链表即可,这样基本就分析完了,get可以参考put的寻找相同key的过程,就不分析了,剩下的扩容时机,怎么扩容的已经分析了,还有和1.7的差别,也简单列举了下,下面再简单总结下
总结
最后总结下吧,HashMap就是数组加链表加红黑树的结构,存入一个值的时候会对key取hash,然后找到对应下标,进行存入,如果没有hash冲突直接存放进数组,数据已存在值的情况下进行拉链表,尾插法,有相同key的时候进行替换对应的值,链表长度超过8进行变化为红黑树,树化的时候先判断下数组的长度是不是小于默认值MIN_TREEIFY_CAPACITY,如果小于MIN_TREEIFY_CAPACITY,那么进行扩容,而不是树化,如果大于等于MIN_TREEIFY_CAPACITY,那么变为红黑树,红黑树节点小于一定数量退化为链表。