之前看过一部分jdk的源码,发现看了之后又忘了,或者是看的不够深入,有次去面试:问我hashmap怎么实现的,结果我答的不怎么样,没答出一些关键部分,现在再重新开读读jdk的util包下的一些源代码:
特别声明:小弟发博客纯属学习,若有错误,不当之处请指出!!!
首先,我们大家都知道hashmap内部用的是散列实现,但是具体怎么实现的呢?如果解决冲突的呢?
实现原理:
hashmap用数组实现,数组中存放的是Entry类型的元素,每个Entry元素其实是一个key-value对,持有下一个元素的引用,这就说明table数组中的Entry元素还是某个Entry链表的首节点,指向该链表的下一个元素,所以hashmap用“数组链表”实现的;
这个是网上找的hashmap底层数据结构的一个图片:
1、初始化
public HashMap(int initialCapacity, float loadFactor) { 有两个参数,一个初始值,一个加载因子,默认情况初始值是16,加载因子是0.75
........
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
this.loadFactor = loadFactor;
threshold = (int)(capacity * loadFactor);
table = new Entry[capacity];
init();
}
2、扩容(重构)
当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的长度是固定的。所以为了提高查询的效率,就要对HashMap的数组进行扩容,数组扩容这个操作也会出现在ArrayList中,这是一个常用的操作,而在HashMap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。
3、解决冲突:在数据结构中有线性散列,平方散列等等方式:
hashmap源码采用的方式是:通过复杂的算法找到要插入的位置如下,如果已经有元素了,就线性的往后查找
hash值的算法:
static int hash(int h) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
在数据结构教材上是直接除以一个数取余,而Java实现却如此复杂,网上查资料:
先看个例子,一个十进制数32768(二进制1000 0000 0000 0000),经过上述公式运算之后的结果是35080(二进制1000 1001 0000 1000)。看出来了吗?或许这样还看不出什么,再举个数字61440(二进制1111 0000 0000 0000),运算结果是65263(二进制1111 1110 1110 1111),现在应该很明显了,它的目的是让“1”变的均匀一点,散列的本意就是要尽量均匀分布。
将得到的值和(length-1)相与,得到的结果就是插入的索引位置
static int indexFor(int h, int length) {
return h & (length-1);
}
具体的插入方法:
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key.hashCode()); 先得到hash值
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;
}
到此就解决了刚刚提出的问题:如果得到索引,如何解决冲突。