花了一些时间阅读了jdk1.8中的HashMap、LinkedHashMap、TreeMap和WeakHashMap的源码,整理一下学习到的东西,这篇博客主要写HashMap的实现源码,并次要总结一下另外三个map实现类的实现原理和特性。
HashMap采用的数据结构
首先介绍一个重要的参数TREEIFY_THRESHOLD,看一下官方备注。
/**
* The bin count threshold for using a tree rather than list for a
* bin. Bins are converted to trees when adding an element to a
* bin with at least this many nodes. The value must be greater
* than 2 and should be at least 8 to mesh with assumptions in
* tree removal about conversion back to plain bins upon
* shrinkage.
*/
static final int TREEIFY_THRESHOLD = 8;
译:使用树而不是列表的容器计数阈值。 将元素添加到至少包含多个节点的元素时,元素将转换为树。 该值必须大于2,并且应该至少为8,以便与收缩时转换回普通箱的树木移除假设相关联。
HashMap使用Node<K,V>作为每个节点对象,每个节点中包括hash值、key值、value值、指向下个节点的引用。
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
HashMap在节点数在小于TREEIFY_THRESHOLD时,使用数组Node<K,V>[]存储头节点,当存入K,V时,若出现hash冲突,在头节点后扩展节点,若不出现hash冲突,在数组Node<K,V>[hash(K)&(tab.length-1)]存入节点,即数组类似一个连续摆放的桶装,而每个桶都存有K,V数据和一个向后指向的指针,类似一个链表,借用别人的图:
HashMap在节点数在大于或等于TREEIFY_THRESHOLD时,使用红黑树TreeNode<K,V>存储节点,当存入K,V时,在红黑树中插入节点并令红黑树平衡,红黑树的特性就不做介绍了,优点很明显,在查、插入、删除操作的效率是非常可观的。为了美观还是附个图:):
ps:在节点的增加减少的过程中,不是简单的在节点数小于或大于TREEIFY_THRESHOLD时进行模型的改造,而是设立了UNTREEIFY_THRESHOLD配合使用,这里我的理解是起到了一个缓冲的效果,如果当节点数在7-8之间频繁的增加减少,会增加重组数据结构的大量开销,设立了两个阈值的好处则可以在节点数在7-8或6-7之间频繁增加减少时有一个比较智能的效果。
HashMap中的重要方法阅读
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) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//table是存储节点的数组
//若表为空或表长为0,扩容表
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//hash(key)的值&(n-1)即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);
//如果节点数大于阈值,改造成红黑树的结构
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;
}
}
//如果找到了与之相同的节点e,进行覆盖操作
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
//doNothing,LinkedHashMap重写了该方法
afterNodeAccess(e);
return oldValue;
}
}
//官方备注是模型改造的次数
++modCount;
//如果实际容量大于等于下次扩容阈值,进行扩容
if (++size > threshold)
resize();
//doNothing,LinkedHashMap重写了该方法
afterNodeInsertion(evict);
return null;
}
remove
前面备注比较详细,这里做简单的备注
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {//先找到该结点
Node<K,V> node = null, e; K k; V v;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;//刚好和头节点key一样
else if ((e = p.next) != null) {//如果与头节点不一样
if (p instanceof TreeNode)//如果是树节点,沿着树找
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {//如果不是树节点,沿着链表找
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);//移除树节点
else if (node == p)//如果刚好是头节点
tab[index] = node.next;//remove头节点,令头节点指向下一个节点
else
p.next = node.next;//如1->2->3变成1->3
++modCount;
--size;
afterNodeRemoval(node);//doNothing,LinkedHashMap重写了该方法
return node;
}
}
return null;
}
resize
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)//在规定大小范围内,double cap
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) {//如果newThr没有被设定
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);//在规定大小范围内,下一次扩容大小=newCap*loadFactor
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//按newCap扩容,生成一个newTab
table = newTab;//扩容table
if (oldTab != null) {//如果oldTab不为空,把oldTab中的内容加入到已扩容的newTab中
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;
}
LinkedHashMap、TreeMap和WeakHashMap浅析
LinkedHashMap中重写了
void afterNodeAccess(Node<K,V> p) { }// move node to last
void afterNodeInsertion(boolean evict) { }// possibly remove eldest
void afterNodeRemoval(Node<K,V> p) { }// unlink
三个方法,备注里面注明了操作,LinkedHashMap是实现了顺序结构的节点,在HashMap中的Node上扩展了before,after,构造了一个按时间先后顺序插入节点的链表.
TreeMap使用了红黑树的数据结构,在put/remove操作后重新调整节点使之平衡.与avl树相比:get时间复杂度为O(log2n),put/remove中的reBalance操作时间复杂度为O(1),而avl的这两个操作时间复杂度分别为O(1),O(log2n),在这样的特性下,红黑树在有大量节点的情况下有更好的表现.
WeakHashMap中使用的弱引用Node(继承了WeakReference),其中的Node节点随时都有可能被gc回收,可以用来做缓存.其中重要的方法有expungeStaleEntries(),在每次调用getTable时都会调用该方法清除掉长时间无用的Node,而在put/remove等方法中均会先调用getTable再进行操作.
学习HashMap总结
HashMap采用的数据结构是(数组+红黑树)和(数组+链表)两种结构变换,当到达TREEIFY_THRESHOLD阈值时,变换成红黑树结构,当到达UNTREEIFY_THRESHOLD阈值时,变换成链表结构.既保证了随节点数增多时操作的效率,又防止了节点数过多导致的各种问题,
如:数组长度过长导致的永久代内存不足,fullGC频繁、数组长度过长导致的连续存储空间利用过多,内存碎片过多无法利用
在子类LinkedHashMap中重写了
void afterNodeAccess(Node<K,V> p) { }// move node to last
void afterNodeInsertion(boolean evict) { }// possibly remove eldest
void afterNodeRemoval(Node<K,V> p) { }// unlink
而在HashMap中使用了这三个方法,避免了在LinkedHashMap中重写put/remove等大型操作而复用大量代码。
另外,在编码风格上,应多加思考,注意代码的扩展性和可读性。(合理的继承、封装、实现接口,命名规则,备注清晰)
LinkedHashMap实现顺序HashMap的思考
LinkedHashMap是扩展了HashMap的Node,增加了before、after指针,在每次put、remove后更改指针重组按时间顺序的链表结构。
那么当节点过多时,问题就很明显了,链表的长度是非常之大的,查询操作就会变成负担,当然如果业务需求只要找到第一个节点或者最后一个节点的话就不会存在这样的问题了。
未来的业务需求是多种多样的,面对这样的情况,可以采用其他方式来实现顺序结构的HashMap,比如采用重建另一个红黑树来按时间顺序存储,即扩展HashMap的Node并增加tparent、tleft、eright指针,并在每次put、remove后更改指针重组按时间顺序的红黑树,另外采用扩展Node加入时间戳的方式我认为也是可行的。