最近一段时间准备沉下心来,认真学习底层的东西。今天从HashMap开始吧。看源码,我个人觉得应该带着问题去看,去学习大师们怎么做的。
{:toc}
- HashMap 底层是用什么数据结构实现的?
- HashMap 怎么扩容?
- HashMap 怎么解决Key冲突?
- HashMap 是线程安全的吗?
- HashMap 是怎么把数据均匀的分布到容器的?
HashMap 底层是用什么数据结构实现的?
我们先来看看,HashMap实现类定义的变量:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
static final Entry<?,?>[] EMPTY_TABLE = {};
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
transient int size;
int threshold;
final float loadFactor;
transient int modCount;
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
可以从上面代码中,看到定义了默认的初始容量,最大容量,默认的容量因子等等。而实现存储的是一个Entry数据,Entry对象的数据结构是什么样的?
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;
}
可以看到这个对象四个属性,K,V,next(可以看作是一个指向下一个数据的指针),一个hash码。那当我们往Map里面put元素的时候,都做些什么样的操作?我们来看一下put的方法:
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
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;
}
从上面代码中,可以看到put的时候,以Key做了一个Hash出来一个hash值,然后根据这个hash值在获取在entry数据中的位置,之后在检查是否有相同的Key,如果有相同的Key就覆盖掉原来的值,返回原来的值;如果没有的话,直接增加到entry数组中,返回空。
看到这里,我们应该可以明白HashMap怎么来存储了。
hashMap是以entry数组来存储的,而为了解决hash冲突,entry内部有一个next属性,指向下一个entry元素。也就是说,如果hash到冲突之后,就存入当前hash entry值的下一个,这样就把一个一个冲突的值链起来来了。
HashMap 怎么扩容?
从PUT方法里可以看出。如果hash出来的值经过indexFor 得出的数组下标,直接进行了addEntry,那我们来跟一下代码:
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
如果table中,该下标已经存在了某个值,并且 当前数组的容量已经大于了设定了阈值了。这时候,就会进行扩容resize方法,而且传给resize的参数是2倍的当前容量。
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, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
resize方法里面做了一些验证,然后把老数据转移(transfer)到了新的数据里面。
HashMap 怎么解决hash冲突?
我们从上面的Put方法我们可以看到,put的时候,检查了遍历了,每个entry的元素及冲突的链表。
如果冲突之后,会把当前值增加到相同的index的entry的后面形成 一个链表形式的冲突值列表。
HashMap 是线程安全的吗?
查看源码,我们没有发现任何与同步有关的方法,hashMap是非线程安全的。Hashtable是线程安全的,但是查看源码只是把所有的方法加了synchronized关键字,性能肯定差很多。
HashMap 是怎么把数据均匀的分布到容器的?
这个需要仔细的研究一下,put方法中的hash方法以及 indexFor 方法。
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).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
/**
* Returns index for hash code h.
*/
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);
}
从代码上面来看,key经过hash,然后有经过了一个&操作;注意上面的一个注释:
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
lenght必须设置成非零且是2的整数倍。为什么是2的整数倍呢?2的整数倍有什么好处?
大家可以看一下这个博客:讲的很清楚,实际上,当lenght为2的整数倍时,h&(length-1)和h%length 是等价的,但是效率上有很大的差别。
来源:oschina
链接:https://my.oschina.net/u/657361/blog/756268