1、HashMap原理
HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
HashMap 实际上是一个“链表散列”的数据结构,即数组和链表的结合体。HashMap 的基本组成单元HashMapEntry,如下,HashMap的静态内部类
static class HashMapEntry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
HashMapEntry<K,V> next;//存储指向下一个HashMapEntry的引用,单链表结构
int hash;//对key的hashcode值进行hash运算后得到的值,存储在Entry,避免重复计算
HashMapEntry(int h, K k, V v, HashMapEntry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
//...
}
其结构图大致如下:
数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的。
如果定位到的数组位置不含链表(当前entry的next指向null),那么对于查找,添加等操作很快,仅需一次寻址即可;
如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找。
2、HashMap初始化
public HashMap(int initialCapacity, float loadFactor) {
//检验initialCapacity、loadFactor
……
threshold = initialCapacity;
init();//init方法在HashMap中没有实际实现,不过在其子类如 linkedHashMap中就会有对应实现
}
initialCapacity:初始化申请空间,默认是4,即数组内部长度是2^4 = 16
loadFactor:扩容因子,默认0.75
threshold:下次扩容时的临界值,在inflateTable的时候确认,等于capacity * loadFactor
其他两个构造最终都是调用上面这个,在构造中,并没有初始化数组table(那个传入一个Map集合的构造除外),而是在执行put操作的时候才真正构建table数组。
public V put(K key, V value) {
//如果table数组为空数组{},进行数组填充(为table分配实际内存空间),入参为threshold,此时threshold为initialCapacity 默认是1<<4(2^4=16)
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
//如果key为null,存储位置为table[0]或table[0]的冲突链上
if (key == null)
return putForNullKey(value);
int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key);//对key的hashcode进一步计算,确保散列均匀
int i = indexFor(hash, table.length);//获取在table中的实际位置
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
//如果该对应数据已存在,执行覆盖操作。用新value替换旧value,并返回旧value
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++;//保证并发访问时,若HashMap内部结构发生变化,快速响应失败
addEntry(hash, key, value, i);//新增一个entry
return null;
}
public static int singleWordWangJenkinsHash(Object k) {
int h = k.hashCode();
h += (h << 15) ^ 0xffffcd7d;
h ^= (h >>> 10);
h += (h << 3);
h ^= (h >>> 6);
h += (h << 2) + (h << 14);
return h ^ (h >>> 16);
}
这里,在put
的时候,调用inflateTable
来初始化table
private void inflateTable(int toSize) {
// capacity一定是2的次幂,即保证数组长度一定是2的次幂
int capacity = roundUpToPowerOf2(toSize);
float thresholdFloat = capacity * loadFactor;
if (thresholdFloat > MAXIMUM_CAPACITY + 1) {
thresholdFloat = MAXIMUM_CAPACITY + 1;
}
threshold = (int) thresholdFloat;// 下次扩容临界值
table = new HashMapEntry[capacity];//初始化哈希表
}
那么为什么HashMap的数组长度一定是2的次幂?
在put()
中,通过transfer()
获取元素在数组中的索引,index是通过key的hash值和length-1
通过位运算得到的,除非hash值一致,得到的index是不一样的,而且能更加均匀的在table中存放,大概率降低冲突。
而且,在resize()
扩容的时候,数组长度发生变化,存储的位置index = h & (length-1)
可能会变化,
void resize(int newCapacity) {
HashMapEntry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
HashMapEntry[] newTable = new HashMapEntry[newCapacity];//创建一张新表
transfer(newTable);//将旧表的数据循环遍历,导入新表
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
void transfer(HashMapEntry[] newTable) {
int newCapacity = newTable.length;
for (HashMapEntry<K,V> e : table) {
while(null != e) {
HashMapEntry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
在transfer()
里面,将老数组中的数据逐个链表地遍历,扔到新的扩容后的数组中,通过indexFor()
重新获取元素在table中的索引位置。正因为数组的长度是2的次幂,所以新表扩容后,在通过位运算获取index时,length-1
的低位全为1,高位为0,因此除非hash值在高位也为0,否则旧表与新表的index保持一致,保证了之前旧表散列良好的数据不会因为扩容发生冲突。
modCount如何保证线程安全,如何能安全的运用HasMap
modCount的意思就是修改次数,在迭代器初始化过程中会将这个值赋给迭代器的expectedModCount,在迭代过程中,判断modCount跟expectedModCount是否相等,如果不相等就表示已经有其他线程修改了Map。
安全使用:
1、使用线程安全的ConcurrentHashMap或HashTable,它就不会产生ConcurrentModificationException异常,也就是它使用迭代器完全不会产生fail-fast机制
2、Collections.synchronizedMap将HashMap包装起来
Map m = Collections.synchronizedMap(new HashMap());
...
Set s = m.keySet(); //不需要加锁
...
synchronized(m) { // 对Map的对象m加锁
Iterator i = s.iterator(); // 必须加锁的模块
while (i.hasNext())
foo(i.next());
}
3、Hash冲突
两个不同的元素,通过哈希函数得出的实际存储地址相同,即Hash冲突。
解决方法:
开放定址法(再散列法):当关键字key的哈希地址p=H(key)出现冲突时,以p为基础,产生另一个哈希地址p1,如果p1仍然冲突,再以p为基础,产生另一个哈希地址p2,…,直到找出一个不冲突的哈希地址pi ,将相应元素存入其中。
再哈希法:同时构造多个不同的哈希函数,当第一个函数生成当hash值冲突的时候,计算第二个哈希函数,直到不冲突为止。这种方法不易产生聚集,但增加了计算时间。
链地址法:将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。
建立公共溢出区:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。
HashMap即是采用了链地址法,也就是数组+单链表的方式