一、概念
HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
二、特点
1.快速存储
2.快速查找(时间复杂度O(1))
3.可伸缩
三、HashMap数据结构
1、数据结构
在java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针引用(链表),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。 在jdk1.8,hashMap数据结构中加入了红黑树,即链表达到一定的长度就转换为了红黑树。
总结:数组、链表、红黑树(jdk1.8)
从上图中可以看出,HashMap底层就是一个数组结构,数组中的每一项又是一个链表。当新建一个HashMap的时候,就会初始化一个数组。
其中Java源码如下:
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
final int hash;
……
}
可以看出,Entry就是数组中的元素,每个 Map.Entry 其实就是一个key-value对,它持有一个指向下一个元素的引用,这就构成了链表。(注意jdk1.8之后换成了node数组,可能是链表也可能是树,当前key位置的值超过8个用树存储)
2、HashMap实现存储和读取
1)put存储
public V put (K key, V value)
//HasnMap允许存放null键和nul值。
//当key为null时,调用putForNullKxey方法,将value放置在数组第一个位置。
if (key == null)
return putForNullKey (value) ;
//根据key的keyCode 重新计算hash值。
int hash = hasn(key.hashCode())):
//搜索指定hash值在对应table中的索引。
int i = indexFor(hash, tab1e.length)
//如果i索引处的Entry不为null,通过循环不断遍历 e 元素的下一个元素。
for (Entry<K,V> e = tab1e[i]; e != null; e = e.next) {
Object k;
if(e.hash - hash && ((k = e.key) == key || key.equals(k))) {
//如果发现己有该键值,则存储新的值,并返回原始值
V oldVa1be = e.valve;
e.value = value:
e. recordAccess (this) ;
return oldValue;
}
}
//如果i索引处的Entry为null, 表明此处还没有Entry。
modCount++:
//将key、value添加到1索引处。
addEntry (hash, key, value, i):
return null;
}
根据上面 put 方法的源代码可以看出,当程序试图将一个key-value对放入HashMap中时,程序首先根据该 key的 hashCode() 返回值决定该 Entry 的存储位置:如果两个 Entry 的 key 的 hashCode() 返回值相同,那它们的存储位置相同。如果这两个 Entry 的 key 通过 equals 比较返回 true,新添加 Entry 的 value 将覆盖集合中原有 Entry的 value,但key不会覆盖。如果这两个 Entry 的 key 通过 equals 比较返回 false,新添加的 Entry 将与集合中原有 Entry 形成 Entry 链,而且新添加的 Entry 位于 Entry 链的头部——具体说明继续看 addEntry() 方法的说明。通过这种方式就可以高效的解决HashMap的冲突问题。
2)get读取
public V get(Object key) {
if (key == null)
return getForNullKey();
int hash = hash(key.hashCode());
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
return null;
}
从HashMap中get元素时,首先计算key的hashCode,找到数组中对应位置的某一元素,然后通过key的equals方法在对应位置的链表中找到需要的元素。
3)归纳起来简单地说,HashMap 在底层将 key-value 当成一个整体进行处理,这个整体就是一个 Entry 对象。HashMap 底层采用一个 Entry[] 数组来保存所有的 key-value 对,当需要存储一个 Entry 对象时,会根据hash算法来决定其在数组中的存储位置,在根据equals方法决定其在该数组位置上的链表中的存储位置;当需要取出一个Entry时,也会根据hash算法找到其在数组中的存储位置,再根据equals方法从该位置上的链表中取出该Entry。
四、hash算法
所有的对象都有hashCode(使用key的)
hash(int h)方法根据key的hashCode重新计算一次散列。此算法加入了高位计算,防止低位不变,高位变化时,造成的hash冲突。
//重新计算哈希值
static final int hash(Object key) {
int h;
//(hashCode) ^ (hashCode >>> 16)
//key如果是null 新hashcode是0 否则 计算新的hashcode
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
我们可以看到在HashMap中要找到某个元素,需要根据key的hash值来求得对应数组中的位置。如何计算这个位置就是hash算法。前面说过HashMap的数据结构是数组和链表的结合,所以我们当然希望这个HashMap里面的元素位置尽量的分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,而不用再去遍历链表,这样就大大优化了查询的效率
为什么是16呢?
参考:HashMap中的hash算法总结_晴天-优快云博客_hashmap的hash算法
三、Hash冲突
Hash冲突:
不同的对象算出来数组下标是相同的
单向链表:用于解决Hash冲突的方案,加入一个next记录下一个节点
四、HashMap的扩容(resize)
触发条件:
数组存储比例达到75% --- 0.75
新建容量翻倍的数组,所有数据重新计算哈希值,放入数组
当hashmap中的元素越来越多的时候,碰撞的几率也就越来越高(因为数组的长度是固定的),所以为了提高查询的效率,就要对hashmap的数组进行扩容,数组扩容这个操作也会出现在ArrayList中,所以这是一个通用的操作,很多人对它的性能表示过怀疑,不过想想我们的“均摊”原理,就释然了,而在hashmap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。
那么hashmap什么时候进行扩容呢?当hashmap中的元素个数超过 数组大小*loadFactor 时,就会进行数组扩容,loadFactor的默认值为0.75,也就是说,默认情况下,数组大小为16,那么当hashmap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知hashmap中元素的个数,那么预设元素的个数能够有效的提高hashmap的性能。比如说,我们有1000个元素new HashMap(1000), 但是理论上来讲new HashMap(1024)更合适,不过上面已经说过,即使是1000,hashmap也自动会将其设置为1024。 但是new HashMap(1024)还不是更合适的,因为0.75*1000 < 1000, 也就是说为了让0.75 * size > 1000, 我们必须这样new HashMap(2048)才最合适,既考虑了性能的问题,也避免了resize的问题。
五、jdk1.8引入红黑树
红黑树:
一种二叉树,高效的检索效率
触发条件
在链表长度大于8的时候,将链表转为红黑树
链表长度小于8的时候,转回链表
六、HashMap是线程不安全的
HashMap在多线程put后可能导致get无限循环;多线程put的时候可能导致元素丢失(有空就测一下)
如何使用线程安全的哈希表结构呢,这里列出了几条建议:
使用Hashtable 类,Hashtable 是线程安全的;
使用并发包下的java.util.concurrent.ConcurrentHashMap,ConcurrentHashMap实现了更高级的线程安全;
或者使用synchronizedMap() 同步方法包装 HashMap object,得到线程安全的Map,并在此Map上进行操作。