核心数据结构
在jdk 1.7中,核心数据结构是哈希表+链表,在jdk1.8中是哈希表+链表+红黑树,链表和红黑树主要是为了解决哈希冲突的问题,红黑树主要解决在哈希冲突比较严重的情况下链表的查询效率问题。
哈希表的初始容量为16,加载因子默认为0.76=5。
初始容量
在哈希表初始化时,有这样一段代码
// Find a power of 2 >= toSize
int capacity = roundUpToPowerOf2(toSize);
table = new Entry[capacity];
作用是向下找到最接近toSize的2的整数次幂,例如toSize=5,那么capacity=8,如果toSize=27,那么capacity=32,之所以这名做,是因为在计算哈希索引的时候,为了避免低效的取模(%)操作,HashMap用了hashcode & (lenght - 1)来实现,这样再保证lenght为2的整数次幂的情况下,hashcode & (lenght - 1)就是一个 0 ~ (lenght - 1)之间的数,保证不会越界。
另外,在扩容的时候也用到了这个特性,来提高扩容效率。
加载因子设置为0.75的原因
记得HashMap源码中有对0.75这个取值的解释,但是刚刚翻开看不知道为什么没有了。这里其实是在时间和空间上的平和,加入加载因子设置为1,理想情况下,所有的元素均匀分布在哈希表中,但是实际情况是这几乎很难,在极端情况下,假设所有元素都被分到了同一个哈希桶中,那么查询效率会很低,但是,如何设置为0.5的话,那么就只有一半的空间会被利用,虽然解决了get的效率问题,但是空间浪费很严重,所以,0.75是一个折中值。
链表长度为8的时候触发链表转红黑树
在jdk代码中下面一段注释
* Because TreeNodes are about twice the size of regular nodes, we
* use them only when bins contain enough nodes to warrant use
* (see TREEIFY_THRESHOLD). And when they become too small (due to
* removal or resizing) they are converted back to plain bins. In
* usages with well-distributed user hashCodes, tree bins are
* rarely used. Ideally, under random hashCodes, the frequency of
* nodes in bins follows a Poisson distribution
* (http://en.wikipedia.org/wiki/Poisson_distribution) with a
* parameter of about 0.5 on average for the default resizing
* threshold of 0.75, although with a large variance because of
* resizing granularity. Ignoring variance, the expected
* occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
* factorial(k)). The first values are:
*
* 0: 0.60653066
* 1: 0.30326533
* 2: 0.07581633
* 3: 0.01263606
* 4: 0.00157952
* 5: 0.00015795
* 6: 0.00001316
* 7: 0.00000094
* 8: 0.00000006
* more: less than 1 in ten million
艰难的翻译了一下,大概意思就是说,哈希冲突的概率遵循一个叫做“泊松分别”的概率理论,在加载因子取值0.75的情况下,链表长度达到8的概率在千万分之一,所以8这个值差不多可以理解为是一个临界点,如果连败哦长度达到8,那么很可能说明hashmap出问题的,这时候为了保证效率,就要将链表转成红黑树。
jdk 1.7 - 扩容导致死循环
这个东东很难用文字说清楚,简单来说,问题的关键在于扩容的时候会导致致链表中的元素倒序排列,再加上一些列的指针操作,扩容完成以后,链表就形成了一个环,最终导致get操作死循环。
jdk 1.8 + 对扩容的优化
对于这个问题,1.8进行了优化,优化包含两方面,一是新的哈希索引的确定策略,二是元素的转移策略,下面这段代码是1.8的扩容逻辑:
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
/**
这里是处理链表结构,这里将一个哈希槽下的链表分成了高低位两种
高低位的确认逻辑是用hashcode & oldLenght
比如:
1010 0101 0101 1011 (hashcode)
0000 0000 0001 0000 (oldLenght 默认16, 2的四次方)
这样得到的结果就是非0即1。
*/
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
//0 按低位算
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
//1 按高位算
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;
}
//高位按 原位置+oldCap 移动到新的数组中,
//这里之所以这名干,也得益于 哈希表的长度是 2的整数次幂
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
上面的逻辑简单理解,就是将链表按高低位拆成两半,然后放到新的数组中,这样就不会出现1.7中链表在扩容完成后顺序翻转的问题,从而避免了产生循环链表,同时,利用哈希表长度为2的整数次幂的特点,避免的rehash的过程,提供扩容效率。