在JDK1.6和1.8下, HashMap底层发生了较大的改变,总结如下。
Ⅰ.JDK1.6下的HashMap底层原理
一、HashMap数据结构
HashMap的底层数据结构是数组+链表,利用数组来实现快速定位,链表来解决哈希冲突。
二、哈希算法
好的哈希算法应尽可能保证计算简单和散列地址均匀,这样可以减少哈希冲突。在HashMap中,哈希算法包括两部分:扰动计算和数组索引计算。
扰动计算 在HashMap中,首先采用扰动函数对key的hashCode进行扰动计算(代码如下):通过若干次的移位、异或操作,把高位的特征和低位的特征组合起来,减少高位不同,低位相同带来的哈希冲突。
static int hash(int h) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
数组索引计算 哈希值对数组长度取余是一个常见的哈希函数,当底层数组的长度为2的n次方时,h % length相当于h & (length-1)。与操作比求余操作效率要高,这也是为什么HashMap的底层数组长度总是2的n次方。
static int indexFor(int h, int length) {
return h & (length-1);
}
三.重要属性
static final int DEFAULT_INITIAL_CAPACITY = 16;//默认数组初始容量
static final int MAXIMUM_CAPACITY = 1 << 30;//默认数组最大容量
static final float DEFAULT_LOAD_FACTOR = 0.75f;//默认装载因子
transient Entry[] table;//存储数据的Entry类型数组,长度是2的幂
transient int size;//保存的键值对数量
int threshold;//需要调整数组大小时的阈值(容量*装载因子)
final float loadFactor;//装载因子
transient volatile int modCount;//map结构被改变的次数
四.put()方法源码
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
在检查节点时,光检查hash值是不够的,因为key相同,hash值一定相同,但是hash值相同,key不一定相同,所以在检查节点时,既检查了hash值也检查了key:e.hash == hash && ((k = e.key) == key || key.equals(k))。有一道经典的面试题也和它类似:“hashcode相等,equals是否相等”,hashcode相等,equals不一定相等,equals相等,hashcode一定相等。
代码详解1.putForNullKey
当存入的键key为null时,HashMap进行一个特殊处理,将它存放在数组索引为0处。
/**
* Offloaded version of put for null keys
*/
private V putForNullKey(V value) {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;//已存在null键时,对value进行覆盖
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(0, null, value, 0);
return null;
}
代码详解2.addEntry
当插入的键key不为null时,通过哈希算法计算对应的数组索引i,遍历相对应的链表,当链表中已经存在键为key的索引时,对value进行覆盖,否则,在链表头部插入新的Entry节点。之后判断键值对数量(size)是否到达了需要扩充table数组容量的界限(threshold)并让size自增1,如果达到了则调用resize(int capacity)方法拓展数组容量。
1 void addEntry(int hash, K key, V value, int bucketIndex) {
2 Entry<K,V> e = table[bucketIndex];
3 table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
4 if (size++ >= threshold)
5 resize(2 * table.length);
6 }
扩容算法详解:resize
当数组容量到达上限时,更新需要调整数组大小时的阈值(threshold)为int的最大值,不进行扩容。每一次扩容时,将数组容量扩展为原来的两倍(满足2的n次方),并将旧数组中的内容迁移至新数组:对链表中的每一个节点,重新计算它在新数组中的索引,并插入到相应的链表头部。最后,设置threshold为新数组容量*装载因子。因此,在JDK1.6中,扩容操作的代价是很大的。
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
transfer(newTable);//将旧数组中的内容迁移至新数组
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}
void transfer(Entry[] newTable) {
Entry[] src = table;
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) {
//遍历数组的每一个索引
Entry<K,V> e = src[j];
if (e != null) {
src[j] = null;
do {
//遍历链表
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];// 将节点e插入到newTable[i]指向的链表的头部
newTable[i] = e;
e = next;
} while (e != null);
}
}
}
五、get()方法源码
public V get(Object key) {
if (key == null)
return getForNullKey();
int hash = hash(key.hashCode());
//计算键值key对应的索引,遍历对应的链表;当节点的哈希值、键值与目标哈希值,键值相等,则该节点为目标节点,返回目标节点的value.
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
return null;
}
代码详解1.getForNullKey
get操作的源码简单,这里就不详细说了。同样的,当查询的键key为null时,仅需在数组索引为0处的链表进行遍历查找。
private V getForNullKey() {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;
}
Ⅱ.JDK1.8下的HashMap优化
JDK1.8从三方面对HashMap进行了优化,
①.底层数据结构由数组+链表变为数组+链表+红黑树:而当链表长度太长(TREEIFY_THRESHOLD默认超过8)时,链表就转换为红黑树。当长度小于(UNTREEIFY_THRESHOLD默认为6),就会退化成链表。
②.优化了哈希算法:在进行扰动计算时,JDK1.6中进行了4次的右位移异或混合操作,而JDK1.8中简化为一次。
static final int hash(Object key) {
//JDK1.8中的扰动计算
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
③.优化了扩容算法:JDK1.8充分利用了一个特性:当数组的容量变为原来的两倍,那么元素的位置要么是原位置,要么是原位置+旧数组容量。