一、什么哈希表?
哈希表也叫散列表(HashTable)是能够通过给定的关键字的值直接访问到具体对应的值的一个数据结构。底层是数组+链表/红黑树,它借助哈希函数对数组这种数据结构进行扩展,利用的是数组支持按照下标随机访问元素的特性,是存储 Key-Value 映射的集合。
二、扩容问题
1.由于哈希算法被计算的数据是无限的,而计算后的结果范围有限,因此总会存在不同的数据经过计算后得到的值相同,这就是哈希冲突,即不同的关键字得到的值可能会相同。
哈希表是java.util.HashMap$Node类型的数组,Node是链表结构,所以一个索引值下可能会有多个元素。所以底层是哈希表的实现类基本上都没有索引值。
如下图:同一个索引值下可能挂了很多个元素,但是为了提高效率,最大不超过八个,方便元素的查找。当添加的元素的索引值下刚好八个时,元素A就会往下一个索引值下面的链表挂,数组默认长度是16,当数组空间不够时,就会扩容,
数组的长度默认为16;
为什么要进行扩容:为了减少哈希碰撞。
如果哈希表中的链太长,也就是哈希冲突比较高的时候,hash表的变量就会变成单链表,效率很低,所以我们要对哈希表进行适当的扩容。
负载因子(LoadFactor)=哈希表的有效元素个数/哈希表长度
负载因子的值越大,就说明冲突越严重,但是数组的的利用率较高(数组中存储的元素很多),反之负载因子越小,就说明冲突越小,数组的利用率越低(数组中存储的元素比较少)。负载因子就是在空间和时间中取平衡值。
哈希表容易出现的问题:
① 如果空间利用率高,那么经过的哈希算法计算存储位置的时候,会发现很多存储位置已经有数据了(哈希冲突);
② 为了避免发生哈希冲突,增大数组容量,就会导致空间利用率不高,所以引出负载因子。
扩容条件:1.同一索引值下元素个数>8,并且数组长度<64;
2.数组的索引值占有率>0.75;
扩容规则:新数组容量==旧数组容量的二倍
转红黑树原则:同一索引值下元素个数>8,并且数组长度>=64.
为什么负载因子是0.75:
//默认的负载因子0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
默认的负载因子0.75是对空间和时间效率的一个平衡选择,一般情况下不要修改,除非在时间和空间比较特殊的情况下,如果内存空间很多而又对时间效率要求很高,可以降低负载因子Load factor的值;相反,如果内存空间紧张而对时间效率要求不高,可以增加负载因子loadFactor的值,这个值可以大于1。
扩容原理:
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
//计算旧哈希表的容量,如果旧的哈希表为空,则长度返回0,否则返回旧哈希表的长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//扩容阈值
int oldThr = threshold;
//初始化新表的长度、阈值
int newCap, newThr = 0;
if (oldCap > 0) {//旧表的长度,如果大于0则代表旧表不为空,即不进行初始化扩容,当容量达到最大的时候,就不在扩容
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//新哈希表的长度扩容到原来的两倍,阈值页变为原来的两倍
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
// 能进入到这里,说明oldCap = 0,也就是初始化扩容,此时扩容的大小就应该是oldThr的值
newCap = oldThr;
else {
//以上条件都不满足,则直接采用默认16长度 ,和12阈值
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
//针对上面没有计算扩容阈值的情况
float ft = (float)newCap * loadFactor;
//判断是否小于最大容量
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
//到这里完成了扩容的长度和阈值的计算,现在开始创建新的hash表
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//将扩容后的哈希表赋值给table
table = newTab;
哈希表判断两个元素是否重复的规则:
哈希值是否相同 && (地址值相同 || equals相同)
三.哈希表应用
Set<E>接口和Map<K,V>接口下几个常用实现类底层都用到了哈希表:
Set接口常用实现类:
1.HashSet:是无序的,底层是哈希表(数组+链表/红黑树)
2.LinkedHashSet:是有序的,底层是链表+哈希表,是有序的
Map接口常用实现类:
1.HashMap:无序,底层的数据结构是哈希表
2.LinkedHashMap:有序,底层的数据结构是链表+哈希表