哈希表,也称散列表,是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫散列函数,存放记录的数组叫做散列表。
在Java中,像HashSet,HashMap等集合的底层数据结构都使用了哈希表,哈希表利用了数组支持按照下标访问的特性,因此散列表类似于一种数组的扩展,在JDK1.8及以后,它由数组+链表/红黑树构成。
以HashSet(HashSet底层实际上是一个HashMap实例)为例,
通过无参构造方法创建HashSet时:
HashSet<String> haset1 = new HashSet<>();
底层会创建一个默认长度为16的Object[],并将加载因子默认为0.75
通过传入int类型指定数组初始化长度时:
HashSet<Integer> set = new HashSet<Integer>(int initialCapacity);
底层会调用tableSizeFor方法
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
通过这个方法,可以得到一个实际数组长度>=传入的参数的最近的一个二的次方的值,然后这个值将会作为数组的长度去创建数组。
调用add方法时,底层会:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
1.先计算出新增元素的哈希值
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
2.通过hash%数组长度得到数组索引
3.判断该数组位置是否为null:
3.1如果为null,则直接以链表形式新增在该数组索引位置
3.2如果不为null,则判断该元素是否是重复的
3.21如果是重复的,就不会新增
3.22如果是不重复的,就新增到这个索引位置的链表的最后面
判断元素是否重复:
if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
这行代码取自HashMap中putVal方法的一小段,可以解释为:
比较两个元素的哈希值&&(地址值是否相同 || equals方法结果)
即:先比较两个对象哈希值,如果哈希值不同,则两个元素不重复,如果哈希值相同,则继续比较地址值或equals方法,如果equals为true,则元素重复,如果为false则不重复。
当哈希表中不同元素哈希值计算相同时,元素通过计算后得到的数组索引值也是相同的,那么根据新增过程,它会被放在数组同一个索引下以链表(是一个单项链表,位于java.util.HashMap<K,V>$Node)的形式继续存储。那么,当链表越长时,哈希表的查找速率也会变得越来越慢,为了解决这一个问题,哈希表采用了扩容机制:
当同一个索引下的元素个数>8个,并且数组的长度小于64时,或者当数组索引值占有率达到加载因子时,就会引发数组扩容机制
扩容新容量 = 旧容量<<1;(即新数组容量为旧数组容量的两倍)
数组扩容后,原数组的元素都会分到新数组中
分法:向新数组长度除2+索引值位置分元素,第一个放在原索引位置,第二个放在扩容后的位置,以此类推,最后一个将放在原索引的位置
由于扩容机制的存在,哈希表的纵深(链表大小)不会超过8个,因此,哈希表可以拥有良好的查找机制以及相对于链表结构更好的增删机制
当同一个索引下元素个数>8个,且数组长度>=64时,当前数组索引位置所在的链表就会树化为红黑树存储(其他索引位置的元素不会发生变化)。
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}