今天从源码角度进一步和hashmap 深入交流。
总所周知,hashmap底层是由数组实现的,更确切的说是由数组+单向链表实现的。而这个单向链表则由Node类中的next来实现。
transient Node<K,V>[] table;
static class Node<K,V> implements Map.Entry<K,V> {
Node<K,V> next;
......
}
首先看一下hashmap的初始化,就看最具代表性的一个。我们需要提供初始容量和负载因子,这个负载因子用来扩容数组,如果数组容量到达了initialCapacity*loadFactor,就会引发数组扩容,这样可以减少不同元素存在同一个数组下标的几率,提高性能。
为什么loadFactor要定为0.75呢
如果太小,0.5的话,浪费了差不多一半的空间,人间不值得。如果太大的话,collision的几率就会变大,插入查找的时间复杂度就会从刚开始的O(1)降为O(n), 总的来说就是空间和时间的妥协。
这个threshold,并不是任意长度都可以,会根据我们给的initialCapacity去计算一个刚好大于这个数的2的幂。具体看源码的艺术:HashMap中的tableSizeFor方法
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);
}
现在我们主要来看一下hashmap是怎么存储数据的。
下面是难点解析
- tab[i = (n - 1) & hash,hash值为hash(key)得来的也就是数组下标,n-1就是数组长度减去1,因为数组长度为2的次方,所以n-1低位全是1,和hash &操作就相当于取模。
- 链表长度太长需要转化为红黑树,因为如果搜索一个无敌长的单链表是超级影响性能的,红黑树则会提高我们的效率。
首先这个put方法,调用了putVal方法,可以看到这里有个hash方法。hashCode方法是Native方法,不管他。hash后的值为h,发现还有一步h^ (h >>> 16)操作。这是为什么呢?
这就是扰动函数
这一步是为了进一步的降低冲突几率。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h;
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;
///如果数组为null,则先创建数组
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;
///如果该数组已存的对象的hash值和key值一样,那么直接赋值,之后判断是否可以把value修改为最新值
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;
}
ok,现在对于hashmap就比较了解了。看完怎么存储值,再看怎么get值就更简单了,就不赘述了~