作为java程序员,hashmap是最常用的类之一了,记得以前面试也经常被问到,记得我当时的回答是把key做hash直接映射到内存地址,hash冲突使用链表解决。今天看了源码记录一下,有个技巧是,HashMap直接debug是看不到很多值的,所以自己把HashMap和AbstractMap拷贝重命名下,就可以使用debug了。
首先理解下以下几点,最后以看下put函数就能明白了。
1 DEFAULT_LOAD_FACTOR = 0.75f; 默认的加载因数,就是扩不扩容什么时刻扩容主要由这个参数决定。
2 transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
底层的那个数组默认是16,可扩容,大小必须是2的平方,存储的是Entry。
3 最重要的hash函数:为了让所有的key,不论是什么类型,都能映射为0-table.length 之间的一个整数
/**
* Retrieve object hash code and applies a supplemental hash function to the
* result hash, which defends against poor quality hash functions. This is
* critical because HashMap uses power-of-two length hash tables, that
* otherwise encounter collisions for hashCodes that do not differ
* in lower bits. Note: Null keys always map to hash 0, thus index 0.
*/
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
//这里会用到所有的高位和地位,为了让任何一位变化都能引起hash变化,是hash散列更均衡
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
/**
* Returns index for hash code h.
*/
static int indexFor(int h, int length) {
// 这里使用了一个技巧,相当于h%length取模,但是位运算效率会高些。
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);
}
4 Entry不仅是一个键值对,还是一个链表,测试存储100个key,使用1-50和随机字符串作为键值,hash冲突是很多的。
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;
}
最后看下put操作:
public V put(K key, V value) {
//检查数组保证已经初始化
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
//对于null健单独存储并替换
if (key == null)
return putForNullKey(value);
int hash = hash(key);
//计算出在table中的索引
int i = indexFor(hash, table.length);
//检测如果hash冲突,是否key已经存在,存在则替换value
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;
}
void addEntry(int hash, K key, V value, int bucketIndex) {
//大小大于length*loadfactor 并当前索引(hahs冲突)被占用时才扩容
if ((size >= threshold) && (null != table[bucketIndex])) {
//扩容会全部rehash计算,比较耗费时间,所以如果知道一开始hashmap很大,可以设置初始化大一些来避免rehash
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
//如果当前索引已被占用时,会先取出来,并将新建的table[bucketIndex] 的next指向e,构成链表。get时只要依次比较key是否相等即可找到对应的Entry。
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
其实应该还有很多细节,感觉也不需要细看,理解原理和数据结构就可以了,一些技巧当然可以借鉴,然后就是有助于以后更好的使用hashmap来写代码。