HashMap
引言
哈希表(HashTable)
也称散列表,是一种非常重要的数据结构,应用场景机器丰富,许多缓存技术的核心其实就是在内存中维护一张大的哈希表,本文会对java集合框架中HashMap的实现原理进行讲解,并对JDK7的HashMap袁爱民进行分析
什么是哈希表
首先来了解下其他的数据结构再讲
- 数组
采用一段连续的存储单元来存储数据.对于制定下标的查找,时间复杂度为O(1),通过给定值进行查找,需要遍历数组,注意对比给定关键字和数组元素,时间复杂度为O(n),当然对于有序数组,正则可以采用二分查找,插值查找,斐波那契查找等方式,可将复杂度提高到O(logn) - 线性链表:
对于链表新增,删除等操作(在找到指定操作位置后,仅需要处理节点之间的引用即可,时间复杂度为O(1),而查找操作需要遍历链表逐一进行比对,复杂度为O(n) - 二叉树:
对一棵数相对平和有序的二叉树,对其进行插入,查找,删除等操作,平均复杂度为O(logn) - 哈希表:
相对上述几种数据结构,在哈希表中进行添加,删除,查找等操作,性能十分之高,不考虑哈希冲突情况下,仅需一次定位即可完成,时间复杂度为O(1)接下啦Hash表是如何时间复杂度最低的
Tip:
数组简单搜下标快,搜元素慢
线性链表增删快,查找慢
二叉树都差不多
哈希表增删查都快
我们知道数据结构的物理存储结构只有两种:顺序存储结构和链式存储结构,而在上面我们提到过,在数组中根据下标查找某个元素,一次定位就可达到,哈希表利用了这种特性,哈希表的主干就是数组.
比如我们要新增或查找某个元素,我们通过吧当前元素的关键字通过某个函数
映射到数组中的某个位置,通过数组下标一次定位就可完成操作
这个函数可以简单抽象为:cunchuweizhi =f(关键字),这个函数f一般称为hash函数,函数的好坏直接影响Hash表的优劣,如下执行插入数据
查找的时候也是同理,先通过哈希函数计算出实际存储地址,然后从数组中对应地址去除即可
哈希冲突
然而Hash并不是完美的.如果两个不同的元素通过Hash函数得出的实际存储地址相同怎么办?也就是说,当我们对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被其他元素占用了,其实这就是所谓的hash冲突,也叫哈希碰撞,前面我们提到过,哈希函数的设计至关重要,好的函数会尽可能的保证计算简单和散列地址分布均匀,但是,我们需要清楚的是,数组是一块连续的固定长度的内存空间,再好的哈希函数不能保证得到的存储地址绝不发生冲突.
那么Hash冲突如何解决呢?哈希冲突解决方案有很多中,开放定址法(发生冲突,继续寻找下一块未被占用的存储地址),再散列函数算法,而HashMap即采用了链地址法,也就是数组+链表的方式.
HashMap的实现原理
HashMap的主干是一个Entry数组.Entry是HashMap的基本组成单元.每一个Entry包含一个key-value键值对.其实所谓Map其实就是保存了两个对象之间映射关系的一种集合.
HashMap中Entry相关代码
//HashMap的主干数组,可以看到就是一个Entry数组,初始值为空数组{},主干数组的长度一定是2的次幂。
//至于为什么这么做,后面会有详细分析。
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
Entry是hashMap中的一个静态内部类.代码如下
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;//存储指向下一个Entry的引用,单链表结构
int hash;//对key的hashcode值进行hash运算后得到的值,存储在Entry,避免重复计算
/**
* Creates new entry.
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
下图表示Entry是怎样存在的
简单来说,HashMap由数组+链表组成的,数组是HashMap的主题,链表则主要为了解决哈希冲突存在的,如果定位到的数组位置不含链表(如当前Entry的下一个指向的是null)那么查找,添加等操作很快,仅需一次寻址即可;如果定位到数组包含链表,对于添加操作,其复杂度为O(n)首先遍历表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过对key对象的equals方法注意比对查找.所以,性能考虑,HashMap中的链表出现越少,性能才会越好
其他几个重要字段
/**实际存储的key-value键值对的个数*/
transient int size;
/**阈值,当table == {}时,该值为初始容量(初始容量默认为16);当table被填充了,也就是为table分配内存空间后,
threshold一般为 capacity*loadFactory。HashMap在进行扩容时需要参考threshold,后面会详细谈到*/
int threshold;
/**负载因子,代表了table的填充度有多少,默认是0.75
加载因子存在的原因,还是因为减缓哈希冲突,如果初始桶为16,等到满16个元素才扩容,某些桶里可能就有不止一个元素了。
所以加载因子默认为0.75,也就是说大小为16的HashMap,到了第13个元素,就会扩容成32。
*/
final float loadFactor;
/**HashMap被改变的次数,由于HashMap非线程安全,在对HashMap进行迭代时,
如果期间其他线程的参与导致HashMap的结构发生变化了(比如put,remove等操作),
需要抛出异常ConcurrentModificationException*/
transient int modCount;
HashMap的数组长度一定是2的次幂
重写equals方法须同时重写hashcode方法
JDK1.8中HashMap的性能优化
假如一个数组槽位上链上数据过多导致性能下降怎么办?
1.8在1.7的基础上针对增加了红黑树进行优化.即当链表超过8是,链表就转换为红黑树,利用红黑树快速增删改查的特点提高HashMap的性能,其中会用到红黑树的插入,删除,查找等算法.
总结(重点)
HashMap在底层数据结构上采用了数组+链表+红黑树,通过散列映射来存储键值对数据因为在查询上使用散列码(通过键生产一个数字作为数组下标,这个数字就是hashcode)所以在查询上的访问速度比较快,HashMap最多运行一对键值对的Key为NULL,允许多对键值对的value为Null.他是非线程安全的,在排序上是无序的