深入理解hashMap
hashMap简介
HashMap是一种以K-V存储的集合对象,结合了数组结构查询快和链表结构插入、删除快的特点;它允许key、value为null
是一种无序并且线程不安全的集合对象。
hashMap初始化
hashMap的实例有两个参数影响其性能:”初始容量”和”加载因子”。容量是哈希表中桶的数量,初始容量知识哈希表在创建时的容量。加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的元素数超出了加载因子与当前容量的乘积时,就要对这个哈希进行rehash,从而哈希表将具有两倍左右的桶数。默认的加载因子为0.75,这是在速度和空间上的折中,加载因子过高减少空间但也增加了查询速度。在设置初始容量时应该考虑元素的总数,以此来减少rehash的次数。当初始容量大于总元素数除以加载因子时就不会发生rehash。
// 默认构造函数。
HashMap()
// 指定“容量大小”的构造函数
public HashMap(int initialCapacity)
// 指定“容量大小”和“加载因子”的构造函数
public HashMap(int initialCapacity, float loadFactor)
// 包含“子Map”的构造函数
public HashMap(Map<? extends K, ? extends V> m)
我们来看一下第三个构造函数代码:
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();
}
void init() {
}
我们可以发现传入了参数并没有给我们初始化,那是不是就说明我们传入的参数是无效的呢?其实这一点是hashMap设计聪明的地方,如果初始化一个hashMap,而不用岂不是占用了空间资源,实际上当我们调用了put方法便会实现初始化。
public V put(K key, V value) {
//table并没有改变,这里为true
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;
}
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize
int capacity = roundUpToPowerOf2(toSize);
//把threshold设置为最小大于输入容量的2的N次放
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
这里会把threshold设置为最小的大于输入容量的2的N次方,这也是之前提的,当rehash时,大约为2倍而不是正好2倍就在于次。而这样做也是另一个hashMap聪明的地方。
前面提到过hashMap的数据结构是数组和链表的结合,所以要尽量让元素分布的均匀一些,我们首先想到的便是取模,但是这样做运算消耗比较大,而它是这样做的:
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);
}
这样做的好处就相当于对它进行取模,具体的大家可以进行一下测试,当数组长度为2的N次方和非2的N次方时的对比,会发现当为2的N次方时分布更加均匀。
hash碰撞
解决hash冲突有拉链法、开放地址法。而hashMap解决hash碰撞使用的是拉链法,当我们往hashMap中put元素的时候,先根据key的hash值,找到这个元素在数组中的位置,然后把它放到对应的位置中。如果当前元素所在的位置上已经有其他元素,则这个位置将会以链表的形式存放,后加入的放在链头,先加入的放在链尾。当需要从hashMap获取元素的时候,首先通过key的hash值找到数组中对应的位置,然后通过key的equals方法在对应位置的链表中找到需要的元素。