HashMap实现原理剖析
目录
1. 背景说明
源码版本: android sdk 28
最近比较忙,利用零散的时间,写这个文章断断续续写了好久, 希望能给去面试或者招人的你带来帮助。
2. HashMap的定义
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
......
}
定义了两个范型的类型K和V,继承自AbstractMap, 实现了Map<K,V> 接口,看一下AbstractMap源码发现也实现的这个接口,至于为什么,你可以去问一下Josh Bloch大神,不过似乎这样更有助于查看整体的代码应该实现的结构。不求甚解…
public abstract class AbstractMap<K,V> implements Map<K,V> {
......
}
2. HashMap的构造函数
源码中有多个构造函数,一个一个看
2.1 第一个最简单的无参数构造函数
static final float DEFAULT_LOAD_FACTOR = 0.75f;
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
直接使用静态默认值来初始化loadFactor,也就是说当数据量达到数组容量的75%时,将执行自动扩容操作,此时默认的数组容易为,预定义的静态值16 , 之所以写成 将1左移4位上面注释说明了,这个值必须是2的幂。
/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
2.2 第二个单参数构造函数
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
}
代码中该方法调用了另一个构造方法,将指定的容量和默认扩容的负载因子做为参数传入,直接略过看他调用的第三个。
2.3 第三个双参数构造函数
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);
}
这个构造方法就是完整的将容量与扩容负载因子做为初始参数传入的方式,该方法做了一个判断,也就是传入的默认数组大小不能超过一个指定的最大值,这个最大值是真的不小。
static final int MAXIMUM_CAPACITY = 1 << 30;
对loadFactor的合法性也做了判断,但并没有判断最大值,理论上应该是大于 0 小于等于1的范围,但他没有加上限判断,你如果愿意传入大于 1的值也无所谓,不过可能会导致性能下降,正常没有人会这么干。
方法中最后一句至关重要,最终的这个容易值是经过计算得出来的,并不是你传入的。
this.threshold = tableSizeFor(initialCapacity);
看一下tableSizeFor该方法的实现
/**
* Returns a power of two size for the given target capacity.
*/
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的N次幂的值,并且刚好可以大于你传入容量的最小的一个值,比如你传入 15,结果就是 16,传入 18结果就是 32.目的是为了将HASHCODE的值转为数组下标时,更易于计算.
结论就是:数组大小应该是得出一个threshold(门槛值),当数量大于这个threshold值的百分比后就自动扩展数组长度,这个百分比值就是上面说的loadFactor。
2.4 第四个构造函数
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
这个就比较简单了,用一个已经有的MAP来初化新的HashMap,并且使用默认的默认扩容负载因子,那么他的初始大小容量是多少呢?
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
if (table == null) { // pre-size
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
if (t > threshold)
threshold = tableSizeFor(t);
}
else if (s > threshold)
resize();
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
第一次使用一个MAP来创建HashMap时,当前table为null,所以他的计算方式为,当前传入的Map的大小除以loadFactor再加1,这个是什么效果呢?
举例说明一下:当前Map有10个元素,默认的loadFactor是0.75, 按照他的计算结果是 14.3.
也就是说他要计算出一个容量值,刚好让当前的10个元素所占空间小于75%。 这样就符合这个loadFactor的要求了。
3. HashMap的下标位置计算
当创建了一个HashMap后,需要将一对(K,V)插入进来时,如何确定该元素所在的数组下标是哪个?
首先能做为key放入Hash的对象必须实现了hashCode方法,这个方法会得到一个int类型的整数, 将这个hashCode的高16位与低16位进行按位异或运算,结果可将低16位中与高16位重复的部分都变为0,原高16位不变, 得到的还是一个int类型的数值.
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
接着看一下put方法的部分实现
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
...............
}
第一次向HashMap添加数据时,table为空, 因此调用resize创建了一个指定容量的数组, 将每一对(K,V)都变成,Node<K,V>结果进行存储,这时为每个key生成了一个hash值。
使用这个数组空间总度长减1与这个hash值做按位与运算,得出来的值是什么呢?
举例说明一下:
现在有一对 (k1,v1) 要插入HashMap,假设k1的hashCode=01010000 11100000 00000000 01101110
首先要经过HashMap的hash方法,得到的hash为:01010000 11100000 01010000 10001110
再用这个hsah值与n-1进行按位与操作,可以得到数组的下标值,计算过程如下:
最后我们就得到了 14 这就是通过hash值计算要存放的数组下标位置的方式。
4. HashMap的插入时冲突解决方式
上面找到数组下标后,如果当前下标对应的数组位置是空,可以直接将Node<k1,v1>放入该位置,但如查当前位置已经有了一个元素时,就需要做冲突解决。
假设当前Node<k1,v1>已经放到数组下标为14的位置,这时又有一对(k2,v2)的hash值计算下标后,刚好也是14,如下图所示,画图太累人了。
接着继续看putVal方法的else部分,即当前位置不为null时的处理逻辑,为了提高可读性,省略不用的代码
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
......
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
.......
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
......
return oldValue;
}
}
这段代码比较简单,也就说,如果当前已经存在的k1的hash值也k2相等,并且内容也完全相等,这时并不是发生哈希地址冲突,而只需要更新内容就可以了,用新的v2的替换掉v1,将旧的值返回调用处。
接下来重点分析,当hash值计算出的下标相同,但内容或hash值不相同时的处理逻辑代码,部分代码段如下:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
......
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
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
......
}
精简后的代码读起来很好理解,首先是一个无限循环,他的作用只是统计当前数组下标中的链表中的Node结点的数量,当数量达到TREEIFY_THRESHOLD时 ,将链表转换为红黑树来存放,这个后面再看。
static final int TREEIFY_THRESHOLD = 8;
整个循环就是一个单向链表的遍历过程,如果找到某一个Node结果的hash值与key和既将要插入的k2完全相等,就直接改变该结点的value=v2, 否则就一直找到最后一个结果,也就是说
(e = p.next) == null)成立,证明后面没有更多结点了,将新来的(k2,v2)转换为Node<k2,v2> ,然后放到链表的最后一个位置(注意: 并不是头插法),此时e=null因此后面的判断并不会执行。
//不执行这个if内部
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
插入成功后效果图如下:
5. HashMap的插入时链表转换红黑树
当某一个链表中已经有7个节点,要插第8个节点时,将触发树的转变方法。如下图:
对应HashMap的putVal部分源码如下:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
......
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
treeifyBin(tab, hash);
break;
}
......
}
最终调用了 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<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);
}
}
这段红黑树构建的代码基于TreeNode来实现,而TreeNode本身是继承自LinkHashMapEntry<K,V>实体,并扩展了二叉树需要的左右子树的指针,以及红黑树的标识位.
static final class TreeNode<K,V> extends LinkedHashMap.LinkedHashMapEntry<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);
}
......
}
首先说明一下与红黑树相关的概念:
二叉搜索树(BST):对于二叉树的任意一个节点来说,节点的左子树的值都比它小,右子树都比它大,如果最中间的值在树根部查找效率会非常高时间复杂度是O(logn),但是当插入的节点是一个有序的数列时,树的形装会变成一条单边线性的存储结构了,此时查找的时间复杂度就变成了O(n)了。
平衡二叉树(AVL):平衡二叉树也是一个二叉搜索树,但是平衡二叉树有个约束条件,就是任意一个节点的左子树和右子树的深度差小于等于1,不会出现单边线性的存储结构,平衡二叉树的搜索要么查找到当前节点就是目标节点,要么比当前节点小往左子树进行搜索,要么比当前节点大往右子树搜索,也就是说在二叉树的每一层,我们只需要跟一个节点进行比较,所以它的的时间复杂度是O(logn)。
这样来看AVL是一个最完美的方式,但是你想要保持树的左右子树的深度差不能能超过1,这就意味着,在追加节点到树中时,需要做大量的调整工作(不断的左旋、右旋),以保证树一直是平衡状态。
那有没有一即能插入容易查找又快的方式吗,那就是红黑树,我理解他是介于BST和AVL之间的一种择中方案,并不是说他不需要调整,只是相比AVL次数大大减少,查找速度基本一样。
先来看一下红黑树的样子,我随便搞的一个树,他可以左右完全不平衡。
红黑树的特点:
- 每个节点要么是黑色,要么就是红色
- 根节点也就是root节点需要是黑色
- 红节点的子节点一定是黑节点(红节点肯定有父节点,且是黑节点)
- 叶子节点和null节点是黑节点(说明了红黑树中一半以上都是黑节点)
- 从红黑树的任意一个节点出发到它的叶子节点,它所经过的黑节点数都是相同的,这就是红黑树所需要实现的平衡(也就是说,当前节点的每一条分支路径,它们所包括的黑节点数是一样的,最差的情况就是红黑相间)
- 新插入的节点是红节点,可能在参与平衡操作时会变成黑节点(加入直接插入的节点就是黑节点的话,那么每插入一个节点肯定都要做旋转或者变色来达到平衡。但是如果新插入的是红节点且它的父节点是黑节点的话,那就直接插入,整棵树还是平衡的,就不需要再做平衡处理了)
接下来我们回过头来继续看putVal方法中最后一断刚才略过的代码段。
在判断需要插入新节点时,先判断p的类型是不是TreeNode,如果是的话表示当前链表已经转变为红黑树了,因为需要调用TreeNode的puTreeVal方法来播入新的节点.
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
........
if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode) //<<<<<< =====此处判断是否为TreeNode
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
......
}
}
putTreeVal方法是标准的红黑树插入算法,可以自己测试,此处又要不求甚解了…
到此,完整的putVal的代码就说明结束了。完整代码如下:
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)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
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
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
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
6. HashMap的get方法实现
相比putVal 之下get方法简单多了,get方法调用了getNode方法,getNode方法同样按上面插入时的规则,使用hash值计算出所在的数组下标,然后根据类型判断是在NodeList中查找,还是在TreeNode中查找目标值,找到后返回Node结果,再将其中的value返回给调用处,此处代码很简单,一看就懂了。
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)
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;
}
7. HashMap的容量扩展方式
为了方便理解,将resize()方法,拆分成多个代码断进行说明:
//代码段1
//将旧的表对应的数组成员变量腾出来,给后面新表用使用
Node<K,V>[] oldTab = table;
//如果是第一次创建则为 0,否则将旧的表中节点数量存下来
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//旧表的实际最大容量存下来
int oldThr = threshold;
//初始化新表的节点数量和实际的阈值大小变量
int newCap, newThr = 0;
//如果不是第一次创建原来的节点数量会大于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)
//如果现有的节点数量X2没有达到上限值,则将新表的阈值扩大到旧的2倍
newThr = oldThr << 1; // double threshold
}
.............
代码段1主要功能是判断如果,旧的HashMap存在的话,将旧的容易扩大2倍,做为新表的阈值,如果已经达到最大容易则不做扩容处理。
//代码段2
else if (oldThr > 0) // initial capacity was placed in threshold
//如果当前为第一次创建,并且已经给定了阈值,设置阈值为初始容量
//这个地方的目的是为了后面创建数组时用newCap做为数组长度
newCap = oldThr;
else { // zero initial threshold signifies using defaults
//如果当前为第一次创建,未给定任何参数,使用默认参数初始化
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
/*
如果新的阈值没有指定,也就是上面的oldThr>0的情况下,需要为新的表指定一个阈值,
这种情况发生的可能性说明旧的表已经创建过了,但是现在都删除空了, 此时可以做缩减空间操作
负载因子也指定了(哪怕用的是默认值0.75),正常情况下这个loadFactor应变是不会大于1的,
也就是说如果newCap如果小于MAXIMUM_CAPACITY,那么ft也一定小于MAXIMUM_CAPACITY,
记得上面我们看过,构造方法中并没有对loadFactor不能大于1做判断,因此此处的三目运算才需要判断两个值,个人认为构造方法应变做一下判断不允许大于1,这里也不会有这个可能了,这时如果没有超出最大值,
则新的阈值就是旧的阈值与钢载因子之积。
*/
if (newThr == 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;
代码段2的主要功能是,当创建HashMap时,为其指定一个数组长度和阈值.
//代码段3
//如果旧表有数据,说明需要做数据迁移操作
if (oldTab != null) {
//循环遍历数组后,根据节点类型判断,如果是链家就再做链表遍历,如果已经形成树,就做树的遍历
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)//只有单一节点,没有后续节点,因此直接赋值给新的数组即可
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)//需要将tree展开后存入新的表
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { //遍历链表中所有节点,分别存入新的表,后面重点说明一下这段代码
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
//多出来的一位如果是1 则在新表中的位置为:原位置+旧表容量,否则位置不变,后面会详细说明。
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;
}
}
}
}
}
代码段3,虽然很长,但是目的很明确,就是将旧的数据遍历出来,按新的表的长度分别插入进去。
后面的链表是如何迁移到新表中的呢,我们看代码中根本没有对newTab[x]做过空的判断。首先空间的扩大都是通过左移一位也就是X2来实现的,那么对应的二进制形式,做按位与操作计算下标时,只有一位是不同的,见下图:
也就是说,原16长度的表,最低四位也,扩容后的最低4位是完全一样的,相差的只是多出来的一个高位,那么在迁移时可能出现原来在同一位置的有可能在新表中还是同样的位置,也有可能是一个新位置,原来相同位置的链表节点,在新表中只有可能出现在两个下标中位置中,不会有第三种可能了,这句代码正是判断多出来的一位,因为长度是16刚好是一个1加4个 0,关断高出来的一位,对应在key的hash的相同位置是否为1,即可以断定是旧位置还是新位置。
if ((e.hash & oldCap) == 0)
画个图来说明一下:
也就是说,旧的同一链表中的节点数据,移动新表中后,要么跟原来一样下标不变,要么就是下标+旧表的容量值的,如上图所示,旧表中下标是7 新表中要么是7要么就是 [7 + oldCap]= 23,这就是最后两个if语句的函义。
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
代码中 hiHead,和loHead分别将原来链表中的结点,找出来,判断一下是在j的位置还是 j+1的位置,然后生成两个链表,最后分别放到 newTab[j + oldCap] 和newTab[j] ,因为在do while过程中已经将一个链家拆分出2个链表(也可能第二个是空)了,因此后面直接赋值即可,画个图来说明while循环的过程.
分拆说明后,再来看完整的原始代码如下:
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
newCap = oldThr;
else { // zero initial threshold signifies using defaults
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;
@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;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((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;
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;
}
8. HashMap与HashTable
通过上面的源码可以清楚看到,代码中没有任何同步的处理,精简而高效的代码用来做高性能的处理很合理,但缺点就是线程不安全的,如果在多线程场景下使用直接用HashTable来替换就好了,不要试途写一个线程安全的HashMap因为写出来后,效果不会比HashTable好。
HashMap 是 HashTable 的轻量级实现,他们都完成了Map 接口.
区别如下:
- HashMap允许将 null 作为 key 或者 value,而 Hashtable 不允许.
- HashMap 把 Hashtable 的 contains 方法去掉了,改成 containsValue 和 containsKey.
- Hashtable 继承自 Dictionary 类实现了Map接口,而 HashMap继承AbstractMap也实现了Map接口.
- HashTable 的方法是 Synchronize 的,而 HashMap 不是,在多个线程访问 Hashtable 时,不需要自己为它的方法实现同步,而 HashMap 就必须为之提供外同步.
- 执行效率上 HashMap要好于Hashtable.