参考:JDK1.8源码
开场白:Hashtable是遗留类,很多映射的常用功能与HashMap类似,不同的是它承自Dictionary类,并且是线程安全的,任一时间只有一个线程能写Hashtable,并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了乐观锁(JDK1.7是分段锁)。Hashtable不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换
从结构实现来讲,HashMap是数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的
HashMap继承图
###HashMap构造方法
public HashMap()
public HashMap(int initialCapacity)
public HashMap(int initialCapacity, float loadFactor)
public HashMap(Map<? extends K, ? extends V> m)
一些成员变量的理解
int initialCapacity;//初始化容量
final float loadFactorloadFactor;//负载因子(扩容时,用来与容量相乘)
int threshold;//阀值(容量大于该值就会扩容,扩大为原来的两倍,但最大不超过MAXIMUM_CAPACITY)
transient int size://容量值
transient Node<K, V>[] table;//实际装载数据的数组,类型为Node<K, V>
构造方法源码:
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)//最大容量为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);//保证容量数值是2的倍数
}
//无参构造方法只是设置了负载因子loadFactor
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
tableSizeFor源码:
/**
* 返回大于等于cap的,最小的,同时是2的n次方的数,最大不超过MAXIMUM_CAPACITY
*/
static final int tableSizeFor(int cap) {
//假设n=5,那最后应该返回8
int n = cap - 1; //n=4
n |= n >>> 1; // n = 0100 | 0010 = 0110
n |= n >>> 2; // n = 0110 | 0001 = 0111
n |= n >>> 4; // n = 0111 | 0000 = 0111
n |= n >>> 8; // n = 0111 | 0000 = 0111
n |= n >>> 16;// ...
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
之所以要保证容量数值为2的倍数,是为了__优化对象根据hash值求其所在数组下标的速度__。例如容量size为4(二进制为100),当新增一个数据是,HashMap会根据key的hash值和(size-1)相与(即hash&011),那得到的结果肯定不会大于3,所以不会造成数组越界,而且&比%具有更高的效率。
####Node类结构
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;
}
...
}
Node类结构很简单,字段有4个,hash值、key值、value值、还有一个Node类型的next字段用于指向下一个节点,从而实现链表结构。
HashMap重要方法
HashMap如何添加数据,如何扩容的呢?答案在putVal和resize方法里。
先看添加数据**putVal()**方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K, V>[] tab;
Node<K, V> p; //tab里的计算出的下标的Node数据
int n, i;
if ((tab = table) == null || (n = tab.length) == 0) {
//如果调用的是无参构造方法,table为空,所以进行扩容
n = (tab = resize()).length;
}
if ((p = tab[i = (n - 1) & hash]) == null) {
//计算出的下标下没有数据,就新建一个Node加进去
tab[i] = newNode(hash, key, value, null);
}
else {
//这是HashMap里面已经有一个同样的key的情况
Node<K, V> e;//存放最终找到的键值key的Node对象
K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k)))){
//该key下只有一个数据的情况
e = p;
}
//下面是hash冲突的情况,分两种,一种是用红黑树结构存,一种是用链表存。
//从下面代码可以看到同一个下标下数据大于等于TREEIFY_THRESHOLD-1时会进行判断是要把链表变成红黑树结构还是扩容
else if (p instanceof TreeNode) {
//如果p是红黑树,就用红黑树的添加方法
e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
}
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
//找不到同hash值同key的数据,新建一个
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // 改为红黑树结构存
treeifyBin(tab, hash);
break;
}
//找到了同hash值同key的数据
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;
}
上面可以看到容量超过阈值就会进行扩容,下面看扩容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) {//当前map数据量大于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;
}
else if (oldThr > 0) { //HashMap创建后第一次扩容
newCap = oldThr;
}
else {//HashMap创建后第一次扩容,调用无参构造方法,oldThr会为0,赋值新的容量为16 阀值为16*0.75=12
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float) newCap * loadFactor;
//如果大于MAXIMUM_CAPACITY(60)就赋值为Integer.MAX_VALUE
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;//把旧数组里的引用置为null,才能让虚拟机回收掉
if (e.next == null) {// 该key值下只有一个数据,用旧的hash与新长度-1的值&,作为下标
newTab[e.hash & (newCap - 1)] = e;
}
else if (e instanceof TreeNode) {//红黑树的插入
((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;
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;
}
来分析下复制链表结构那段代码
那里把hash值与oldCap的值为0的分为一条链表,不等于0也分为一条链表,最后把等于0的链表放到原来的下标j里,不等于0的放在j+oldCap下标里。为什么这么做的?
我们知道求数据所在的下标,是通过key的hash值&(oldCap-1),而那里是hash&oldCap。
举个例子,原来容量为oldCap为4(二进制0100),有两个数据key的hash值分别为1,5,那么根据hash&(oldCap-1)求其下标,1&0011=1,101&011=1,那它们都会放在数据下标为1那。
当扩容了,新的容量newCap为8(为原来的2倍),根据hash&(newCap-1)求其下标,1 & 0111=1,101 & 0111=101=5,可以看到hash值1还是放在了下标为1那,hash值为5的放在了下标为5的地方,刚好是1+oldCap。
归根到底还是HashMap把容量设置成2的次方数,扩容策略为原来容量的2倍的前提条件,使得容量size会是001000…的形式,容量size-1低位会是1111这样的形式,扩容为2倍,实际就把数左移一位,新的容量值newCap-1,会比原来容量值oldCap-1多一位1,那么hash & oldCap是求hash值在多出的那一位是0还是1,如果是1,复制后的下标是原来下标+oldCap