哈希表结构存储过程
1.HashMap底层数据数据结构:哈希表
2.jdk7:哈希表 = 数组+链表
jdk8:哈希表 = 数组+链表+红黑树
3.
先算哈希值,此哈希值在HashMap底层经过了特殊的计算得出
如果哈希值不一样,直接存
如果哈希值一样,再去比较内容,如果内容不一样,也存
如果哈希值一样,内容也一样,直接去重复(后面的value将前面的value覆盖)
哈希值一样,内容不一样->哈希冲突(哈希碰撞)
4.要知道的点:
a.在不指定长度时,哈希表中的数组默认长度为16,HashMap创建出来,一开始没有创建长度为16的数组
b.什么时候创建的长度为16的数组呢?在第一次put的时候,底层会创建长度为16的数组
c.哈希表中有一个数据加[加载因子]->默认为0.75(加载因子)->代表当元素存储到百分之75的时候要扩容了->2倍
d.如果对个元素出现了哈希值一样,内容不一样时,就会在同一个索引上以链表的形式存储,当链表长度达到8并且当前数组长度>=64时,链表就会改成使用红黑树存储
如果后续删除元素,那么在同一个索引位置上的元素个数小于6,红黑树会变回链表
e.加入红黑树目的:查询快
外面笔试时可能会问到的变量
default_initial_capacity:HashMap默认容量 16
default_load_factor:HashMap默认加载因子 0.75f
threshold:扩容的临界值 等于 容量*0.75 = 12 第一次扩容
treeify_threshold:链表长度默认值,转为红黑树:8
min_treeify_capacity:链表被树化时最小的数组容量:64
1.问题:哈希表中有数组的存在,但是为啥说没有索引呢?
哈希表中虽然有数组,但是set和map却没有索引,因为存数据的时候有可能在同一个索引下形成链表,如果2索引上有一条链表,那么我们要是按照索引2获取,咱们获取哪个元素呢?所以就取消了按照索引操作的机制
2.问题:为啥说HashMap是无序的,LinkedHashMap是有序的呢?
原因:HashMap底层哈希表为单向链表
LinkedHashMap底层在哈希表的基础上加了一条双向链表
1.HashMap无参数构造方法的分析
//HashMap中的静态成员变量
static final float DEFAULT_LOAD_FACTOR = 0.75f;
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
解析:使用无参数构造方法创建HashMap对象,将加载因子设置为默认的加载因子,loadFactor=0.75F。
2.HashMap有参数构造方法分析
HashMap(int initialCapacity, float loadFactor) ->创建Map集合的时候指定底层数组长度以及加载因子
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);//10
}
解析:带有参数构造方法,传递哈希表的初始化容量和加载因子
- 如果initialCapacity(初始化容量)小于0,直接抛出异常。
- 如果initialCapacity大于最大容器,initialCapacity直接等于最大容器
- MAXIMUM_CAPACITY = 1 << 30 是最大容量 (1073741824)
- 如果loadFactor(加载因子)小于等于0,直接抛出异常
- tableSizeFor(initialCapacity)方法计算哈希表的初始化容量。
- 注意:哈希表是进行计算得出的容量,而初始化容量不直接等于我们传递的参数。
3.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;
}
8 4 2 1规则->无论指定了多少容量,最终经过tableSizeFor这个方法计算之后,都会遵循8421规则去初始化列表容量为了存取高效,尽量较少碰撞
解析:该方法对我们传递的初始化容量进行位移运算,位移的结果是 8 4 2 1 码
- 例如传递2,结果还是2,传递的是4,结果还是4。
- 例如传递3,结果是4,传递5,结果是8,传递20,结果是32。
4.Node 内部类分析
哈希表是采用数组+链表的实现方法,HashMap中的内部类Node非常重要,证明HashSet是一个单向链表
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
解析:内部类Node中具有4个成员变量
- hash,对象的哈希值
- key,作为键的对象
- value,作为值得对象(讲解Set集合,不牵扯值得问题)
- next,下一个节点对象
5.存储元素的put方法源码
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
解析:put方法中调研putVal方法,putVal方法中调用hash方法。
- hash(key)方法:传递要存储的元素,获取对象的哈希值
- putVal方法,传递对象哈希值和要存储的对象key
6.putVal方法源码
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
解析:方法中进行Node对象数组的判断,如果数组是null或者长度等于0,那么就会调研resize()方法进行数组的扩容。
7.resize方法的扩容计算
if (oldCap > 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
}
解析:计算结果,新的数组容量=原始数组容量<<1,也就是乘以2。
8.确定元素存储的索引
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
解析:i = (数组长度 - 1) & 对象的哈希值,会得到一个索引,然后在此索引下tab[i],创建链表对象。
不同哈希值的对象,也是有可能存储在同一个数组索引下。
其中resize()扩容的方法,默认是16
tab[i] = newNode(hash, key, value, null);->将元素放在数组中 i就是索引
i = (n - 1) & hash
0000 0000 0000 0000 0000 0000 0000 1111->15
& 0&0=0 0&1=0 1&1=1
0000 0000 0000 0001 0111 1000 0110 0011->96355
--------------------------------------------------------
0000 0000 0000 0000 0000 0000 0000 0011->3
0000 0000 0000 0000 0000 0000 0000 1111->15
& 0&0=0 0&1=0 1&1=1
0000 0000 0001 0001 1111 1111 0001 0010->1179410
--------------------------------------------------------
0000 0000 0000 0000 0000 0000 0000 0010->2
9.遇到重复哈希值的对象
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
解析:如果对象的哈希值相同,对象的equals方法返回true,判断为一个对象,进行覆盖操作。
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
解析:如果对象哈希值相同,但是对象的equals方法返回false,将对此链表进行遍历,当链表没有下一个节点的时候,创建下一个节点存储对象.