您的位置:首页 > 编程语言 > Java开发

HashMap源码分析

2017-11-27 16:14 134 查看

HashSet、HashMap

概述

分别实现了Set和Map接口,底层用哈希表来存储,迭代时是无序的。

add、remove、contains、size方法是常数时间的,遍历是O(N+M)的(N表示桶数,M表示元素个数)。

都是非线程安全的,类似的也可以用Collections.synchronizedSet转化为线程安全的容器。

HashSet实质上也是基于HashMap来实现的,只是map中的value为static final的object,因此主要分析HashMap即可。

影响HashMap性能的主要有两个参数,初始容量(capacity)和装载因子(load factor)。capacity决定初始时桶的数量,要求是2的倍数,默认是16;loadfactor决定了哈希表能装多满才进行扩容,默认是0.75。

哈希表是由链表数组实现的,每个链表的元素超过阈值(TREEIFY_THRESHOLD = 8)时会将对应的链表转化为红黑树结构,来提高查找效率。当低于另一个阈值时(UNTREEIFY_THRESHOLD = 6)又会转化回链表结构。

树桶(Tree bins)的节点是内部的TreeNode类,按照红黑树的结构组织,内部有treeify方法将链节点转化为树,也有untreeify方法将树转化为链节点。链桶的节点时内部的Node类,就是普通的单向链表。

源码分析

构造器

public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
// 检查参数合法性后,再进行设置
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;
this.threshold = tableSizeFor(initialCapacity);
}
// 计算大于等于cap的2的n次方数,其实就是通过为运算来加速
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}


构造器逻辑比较简单,就是判断一些参数合法性后进行设置。桶的容量设为2的幂是为了提高查找的效率,对于普通的数查找对应的hash桶时一般是通过取余来映射,如果是2的幂,通过h&(capacity-1)的位运算即可实现,这种方式显然效率更高。

2. hash

hash方法用于HashMap内部对对象的hashcode的处理,能提高散列效果:

// HashMap的桶是2的幂,如果直接取余,当capacity较少,hashcode的低位
// 比较相似时,会产生较多冲突。这里通过右移16位再异或,这种方式能更
// 好地利用hashcode的高位,使散列更均匀,且时间复杂度也较低
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}


resize

resize方法是初始化或者再散列时调用的方法,是HashMap中比较核心的一个方法:

// 这个方法在初始化或者扩容时会调用。初始化时,会根据设定的capacity来分配桶。
// 扩容时,由于桶数都是2的幂,因此扩容后原来的节点索引可能不变,也可能移动2的幂次位,
// 如capacity=4,5%4=1,3%4=3,扩容后capacity=8,5%8=5,移动了4位,3%8=3,不变
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 从这里可以看出,扩容后是原来的两倍
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
// 初始化,使用设定的threshold
newCap = oldThr;
else {               // zero initial threshold signifies using defaults
// 初始化,使用默认的threshold(16)
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
// 上面这部分代码其实都是计算resize后的capacity和threshold
@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) {
// 这里将原表指向该元素的引用设为null,可以避免内存泄漏
oldTab[j] = null;
if (e.next == null)
// next为空说明该桶只有单个节点
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
// 如果节点是TreeNode,则进行切分
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
// 如果是链表,也要按情况进行拆分,参考split方法,思路是类似的
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} 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;
}

// 这个方法是TreeNode的实例方法,由HashMap的resize方法内部调用。将树桶切分成“高位桶”
// 和“低位桶”,并且切分完后根据节点数进行treeify或untreeify。
// 为什么切分成“高位桶”和“低位桶”?结合HashMap的特性考虑,capacity都是2的幂,映射到的桶
// 的索引实质上就是看key最右边的几位。表扩容后是扩为原来的2倍,即1右移了一位,扩容后映
// 射到的新索引只有两种情况,只需看新的那一位是不是0,如果为0,说明新表中索引不变,加入
// “低位桶”,如果为1,说明索引移动了k位,加入“高位桶”
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
TreeNode<K,V> b = this;
// 分别保存“高位桶”和“低位桶”的首尾节点
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) {
// 遍历节点,按照hash&bit是否为0,分别加入两个桶
next = (TreeNode<K,V>)e.next;
e.next = null;
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;
}
}
// 后面就是根据节点数来进行treeify或者untreeify了
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);
}
}
}


列了两个主要的相关方法,虽然很长,但看看注释理清思路就差不多了。这里没有给出TreeNode的treeify和untreeify的具体代码,其实就是红黑树和链表的一些相关操作了,大概知道原理即可。

另外也可以看到,这里都是线程不安全的,又有很多链表操作,多线程环境来修改的话,可能会造成一些意外结构,形成死循环,相关的内容网上也很容易看到。

4. put

public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
// put的核心方法,返回原key对应的value或者null
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
// 第一次put时会走这个逻辑
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
// 如果对应的桶为空,直接添加到该桶即可
tab[i] = newNode(hash, key, value, null);
else {
// 否则,在该桶里找到key对应的节点,或者新建节点
Node<K,V> e; K k;
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);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
// 超过阈值要进行treeify
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
// 如果不为空,说明存在该key对应的节点,则进行更新
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
// 这是回调方法
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
// 如果添加元素后超过阈值,要进行resize
resize();
// 这是回调方法
afterNodeInsertion(evict);
return null;
}


虽然也挺长,但方法的流程还是比较清晰的,就是啰嗦了点。

使用HashMap时,需要注意作为key的类的equals和hashcode要保持一致,即如果两个对象是equal的,则也应该有相同的hashcode。这从put的代码就可以看出来原因。一个key是根据hashcode来映射的,如果两个对象在业务上应该是equal的,但有不同的hashcode,则节点可能在不同的桶中,必然会造成混乱。

4. get

public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
// 如果是树节点,则调用树的get进行查找,其实也就是红黑树的查找逻辑了
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
// 否则的话就是线性遍历链表来查找了
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}


get相对而言就比较简单了,如果是链表直接遍历即可,如果是树节点,就要调用TreeNode的方法来get了,其实就是红黑树的查找逻辑了。

另外还有HashMap的remove方法,整体逻辑跟get其实很相似,只是最后不是更新value,而是一些引用的调整。

总结

上面就把HashMap的比较重要的方法源码过了一遍,整体上还是比较清晰的,跟经典的HashMap实现方法也比较一致。

1.8开始HashMap才加入了TreeNode的结构,来改善单个桶链表很长时的查找性能,而且还简化了其他的一些细节,感觉更容易理解。

HashMap的一些实现细节还是很值得学习的,比如对capacity限制为2的倍数,这在mapping、resize时都可以通过位运算来提高效率!

类库源码比较注重效率,所以可能有些地方比较臃肿和冗余,重点应该学习算法思想和组织框架。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  hashmap 源码 java