HashMap (jdk1.7)
HashMap的存储结构是 数组 + 链表 的组合.
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
数组 table 存储的是 Entry 对象, 而 Entry 对象是 HashMap 内定义的一个静态内部类, 可以组织成链表的形式.
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
/**
* Creates new entry.
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
...
}
构造器
//带参构造
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;
threshold = initialCapacity;
init();
}
//无参构造
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
由上面这两个构造器可以看出, HashMap 初始化时只是设置了两个属性值, 并没有构造出一个二维的结构.
put() 操作
put 简单来说就是创建一个映射关系, 开始时会为 table 申请空间, threshold 同时会被赋值为 capacity*loadFactor
public V put(K key, V value) {
//为 table 申请空间, 感觉放在这里挺奇怪的, 这种延迟初始化有什么好处呢
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
//处理 null 键, 要注意的是, null 键会自动映射到table[0]
if (key == null)
return putForNullKey(value);
//对 key.hashCode() 疯狂操作, 得到 hash 值
int hash = hash(key);
//这里的i代表桶号,值得注意的是,并不是用%运算来定位桶的.
int i = indexFor(hash, table.length);
//for 循环来判断重复键, 如果重复就覆盖, 并返回oldValue
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++;
//无重复, 首插法插入k-v
addEntry(hash, key, value, i);
return null;
}
当 hash 值相同时, 会去比较 key 的地址和 equals() 方法
inflateTable(threshold);
table 在这个方法中被申请到空间, 同时threshold 被赋值为 capacity*loadFactor
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize
int capacity = roundUpToPowerOf2(toSize);
//threshold 被重新赋值
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity]; //申请数组
initHashSeedAsNeeded(capacity);
}
int i = indexFor(hash, table.length);
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);
}
这里太巧妙了, 如何散列到桶上, 我只能想到取模运算. 而源码中用的是取末几位来确定, 这样运算效率就高了很多.
这里也是 capacity 要求为 2 的次数的原因. 这种情况下 table.length - 1
的 二进制每位全是1, 保证 key 可以均匀的散列到每个 bucket 里.
addEntry(hash, key, value, i);
void addEntry(int hash, K key, V value, int bucketIndex) {
//扩容的条件 size >= threshold
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
//首插法, 新添加的 entry 会被放到数组上
createEntry(hash, key, value, bucketIndex);
}
get() 操作
public V get(Object key) {
if (key == null)
//遍历 table[0]
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
这里要注意的是, key 为 null, 它直接就去table[0] 中查找
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
//同样对key疯狂操作得到hash值.
int hash = (key == null) ? 0 : hash(key);
//因为下面用不到i,所以散列值i放到for里
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
//根据has重复时,可以比较key地址和equals()
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
这里要注意的是, 如何根据 key 在链表中寻找到 value?
因为同一个链表上, hash值相等. 这时候就要用 key 同 entry 对象中的 key 挨个比较, 地址相同则相同, 地址不同, 用equals 方法来确定
总结
首先, 哈希表是在 put 内部完成空间申请的, 如果 put 的内容是 null, 则直接放到 0 号桶里, 不是 null, 根据 hash 值确定桶号, 用 hash 值, ==运算和 equals 方法确定元素是否重复, 重复就覆盖. 否则插入到链表首部.
“resize 和 可能导致的死锁原因以后哪天有空再学, 优先级比较低”