常量定义
hashmap实现是存储键值对的一种数据结构。首先看它的声明:
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable{}
可以得知HashMap是继承自抽象类AbstractMap且实现了Map接口。(这里有一个疑问,既然抽象类AbstractMap也是实现了Map,是否可以直接写成:
public class HashMap<K,V> extends AbstractMap<K,V>
implements Cloneable, Serializable{}
看下HashMap对应的一些常量,在此之前我们需要对hashmap的存储结构有所了解。实际是Node<K,V>[] table,数组的每个节点为一个Node链表,每个Node存储着键值对。我们将数组的每个元素称为bucket。Node的结构体如下,存储了Key的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;
HashMap内部定义的常量:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //
static final int MAXIMUM_CAPACITY = 1 << 30; //,
static final float DEFAULT_LOAD_FACTOR = 0.75f;//
static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;
- DEFAULT_INITIAL_CAPACITY:默认的buckets数目,值为16;
- MAXIMUM_CAPACITY :最大的buckets数量,如果当前的buckets数量大于这个值,再次调用resize()将不会重新分配buckets的大小。
- DEFAULT_LOAD_FACTOR:默认的rehash触发因子,键值对的数量大于buckets数量的75%,将发生resize()
- TREEIFY_THRESHOLD: 大于等于这个值,该bucket下的链表将发生treeifyBin,从链表转化成红黑树。
- UNTREEIFY_THRESHOLD: 小于等于这个值,该bucket下的节点如果是treenode,将转化成链表。
- MIN_TREEIFY_CAPACITY:如果在treeifyBin的时候,当前的buckets数量小于这个值,将发生resize();
实际上这种场景应该是很难触发的,在总数小于64,每次扩容两倍的情况下,就是32个buckets(16->32)根据0.75的加载因子,最大为24个键值对。即32个桶放了24个球,居然有8个球挤在一个桶里。这里的处理的确体现了作者的严谨。
常用方法
HashMap最常用的就是get,put 两个方法。下面我们拿一个简单的例子来说明HashMap的工作流程,现有如下代码:
HashMap<String,Integer> map = new HashMap<>();
map.put("abc",123);
map.put("def",456);
Integer result = map.get("abc");
Boolean b1 = map.containsValue(123);
Boolean b2= map.containsKey("def");
map.forEach((key, value) -> {
System.out.println(key+value);
});
map.remove("123");
首先调用默认构造函数:
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
put方法和注释:
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将第一次resize();
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
//若p为null,则当前的桶是空的,可以直接插入数据。
tab[i] = newNode(hash, key, value, null);
else {
//这个hash值对应的桶之前已经有数据了。
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)
//这里如果node是TreeNode的实例,则这个链表就已经变成红黑树了。
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//循环查找链表的节点key是否和当前的key相等,binCount为链表长度,大于TREEIFY_THRESHOLD 触发treeifyBin
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;
//已有相同的key插入,直接覆盖即可。
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;
//size 为当前map存储的node数量,用来判断是否需要resize
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
说下几个自我感觉的重点:
-
hash运算时h >>> 16 的作用:
hash方法取到的值还不是桶的数量,实际是取了hash值的低位(如果是16个buckets,那就是4bit , 32则是5bit ...),h >>> 16 让hash值的高16位和低16位异或运算,可以让object的hash值的各位最大程度上参与运算。 -
为什么HashMap可以存储null值?
1.null的hash函数做了特异处理,为0,即放到第一个bucket中。
2.对null值做了判断 用p.hash == hash && ((k = p.key) == key
的方式让null值可以被更新。 -
原本在不同buckets中的node,通过resize()会放到同一个bucket中吗?
不会,resize()每次增大一倍,每个节点放置的bucket位置重新计算,如果旧table该节点在位置table[i],则新位置可能在table[i]或者table[i+oldCap]; 举个例子:buckets数量由16扩展至32,原node A在位置table[2],A的hash值是10010,则分配到新的table[2+16]。如果是01011,则依然停留在table[2]。而且不改变原node链表的相对位置关系。 -
resize()很耗时,因此在初次分配的时候指定好大概大小?
resize()操作实际发生次数是比较少的,单次resize()是比较占用cpu,但只是一瞬间,resize()的时间复杂度是o(n)*平均链表长度 = o(n)。实际业务中,HashMap存储的数据量一般不会太大,大的数据量操作一般是借助存储服务来完成,所以实际上对性能是不会产生太大影响的,当然初次分配了一个合理的预估值,会减少一定的cpu时间。 -
接下里的get 和remove也很简单了,分为以下步骤:
(1) hash要查找/删除的key,找到对应的bucket
(2) 循环链表找到要查找/删除的节点并返回/删除,其中删除的实现:pre.next = node.next;
注意下remove中几个参数的意思:node:要查找的目标节点;p:,如果node是bucket中的第一个节点,pre 即是node,否则是node的pre节点;matchValue:是否需要key,value都匹配才删除。
get的代码:
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;
}
remove的代码:
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;
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;
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
containsValue和containsKey方法
因为HashMap是按照Key来排序的,调用containsValue将循环遍历所有node来查找,效率低下,不建议过多使用containsValue函数。
forEach方法
因为HashMap不是线程安全的,如果在forEach执行过程中table中的内容发生了变化。modCount会发生变化,此时抛出一个ConcurrentModificationException,实现思想和乐观锁一致。
HashMap的迭代器。
附示例代码
HashMap<String,String> ss = new HashMap<>();
ss.put("1","a");
ss.put("2","b");
Iterator<Map.Entry<String ,String>> iterator = ss.entrySet().iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
我们可以通过HashMap.entrySet()来获取一个该对象的Entry集合entrySet变量,开始我很疑惑,HashMap中并没有对这个变量做增加元素等操作。那怎么能平白产生数据呢?后来看到了HashMap的内部类复写了EntrySet的iterator方法。返回的是这个类对象。
final class EntryIterator extends HashIterator
implements Iterator<Map.Entry<K,V>> {
public final Map.Entry<K,V> next() { return nextNode(); }
}
其中nextNode实现
final Node<K,V> nextNode() {
Node<K,V>[] t;
Node<K,V> e = next;
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
if ((next = (current = e).next) == null && (t = table) != null) {
do {} while (index < t.length && (next = t[index++]) == null);
}
return e;
}
这下明白了,变量entrySet并没有存储实际数据,只是通过iterator方法返回了Hashmap中的table存储的Node。需要注意的是,iterator对象中存储了table的位置,和当前节点的引用。这样在调用next的时候才可以返回下一个值。
LinkedHashMap
LinkedHashMap继承自HashMap,比其多了一个功能,就是可以按照插入顺序遍历出元素。原理: LinkedHashMap内部维护了一个链表,且重写了newNode方法,在新建节点的同时,就已经完成了链表的维护.LinkedHashMap#Entry<K,V>#newNode
代码如下:
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
LinkedHashMap.Entry<K,V> p =
new LinkedHashMap.Entry<K,V>(hash, key, value, e);
linkNodeLast(p);
return p;
}