JDK1.8 易懂的HashMap原理
数组是什么?
它是存取有序的容器,在内存空间是一块连续的空间,正因为连续所以长度固定,每个分量固定大小,只能存储同类型,可以以索引进行查找,增删慢,查找快
链表是什么?
它是存取有序的容器,在内存空间并不是一块连续的空间,长度可变,可存储不同类型。每个节点都有指向下一个节点的指针,所以增删快,查找慢。
HashMap是什么?
它由数组加链表组成。所以同时拥有数组和链表的优点:查找又快,增删又快,就是有点耗内存,大道理先别讲这么多,不然容易懵逼。
首先看看且记记实例化HashMap的几个字段:
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable{
//序列化ID
private static final long serialVersionUID = 362498820763181265L;
//默认初始容量,也就是你自己实例化不设置的话,人家就给你这个大小
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认加载因子,就是说如果容器的容量*加载因子>=临界值时将会进行自动扩容
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//链表节点数大于这个值时将会自动转成红黑树
static final int TREEIFY_THRESHOLD = 8;
//红黑树节点数小于这个只是将会自动转成链表
static final int UNTREEIFY_THRESHOLD = 6;
//桶中结构转化为红黑树对应的table的最小大小
static final int MIN_TREEIFY_CAPACITY = 64;
//存储Node的数组
transient Node<K,V>[] table;
//存储键值对的集合
transient Set<Map.Entry<K,V>> entrySet;
//已经存放元素的个数
transient int size;
//每次扩容和更改map结构的计数器
transient int modCount;
//临界值
int threshold;
//加载因子
final float loadFactor;
...
}
Node是什么?
它实现了Entry接口
Node的field:
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
...
其中有个Node类型的next,field,next为Node,Node的next又为Node,没错这就是链表怎么构成的原因。
如果你认真看了上面一大坨字段的话,那下面这个图估计一看就明白,看看HashMap是这么将数组和链表组合起来的:
这个红黑树节点总数肯定是大于6的,我不画了,因为在画,图片就看不清了。
为什么TreeNode能放到Node数组里的呢?因为TreeNode是最终还是继承Node的呀,看下结构吧:
static final class TreeNode<K,V> extends LinkedHashMap.LinkedHashMapEntry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
...
先不讲为什么需要链表,
我们都知道,hashMap是以key来进行存取的,key不能重复,也就是说你再存一个重复key的Node,将会覆盖相同key的Node。
hashMap是通过key查找值,最理想的时间复杂度为O(1),那哪个容器查找时时间复杂度能达到O(1)呢?没错就是数组,也就是说我们时用key时对应数组的索引的。下面我们看一下它是这么进行对应的:
hashCode():
先不直接贴这个方法源码,咱们应该从头追踪进去,这样才能更好验证我们所说的
我们先看存数据时的逻辑:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
里边调用了hash(key)
,
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
key为空时直接返回0即对应数组的下标0位置,否则进行hash运算,这里只需知道通过hash算法我们的到了对应数组的下标位置,hash算法越好,元素再数组的分布越均匀,但是也难免会有冲突(即算出来的下标位置上已经有元素了),至于HashMap里的hash算法的详细逻辑就需要另起一篇幅讲了,不然文章就很长了。
那现在已经拿到了下标位置我们看看咱们put进去的逻辑:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
Node<K,V>[] tab;
Node<K,V> p;
int n, i;
//table就是Node数组
//如果为空或者长度为0,就执行resize(),就是对
//Node数组的创建或者扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//判断下标位置上有没有元素,没有就直接插入
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//有元素
else {
Node<K,V> e; K k;
//如果发现key相等。
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//将元素赋值给e
e = p;
//key不相等
//如果该位置的元素是TreeNode,说明是个红黑树,那就得用红黑树的插入方法进行插入
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//否则插入链表
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;
}
//如果发现key值相等,则退出循环
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 如果中找到key值、hash值与插入元素相等的结点,则进行替换value
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
//插入或替换后的逻辑
//修改次数加加
++modCount;
//判断是否节点总数是否大于阈值,大于就扩容
if (++size > threshold)
resize();
//执行回调
afterNodeInsertion(evict);
return null;
}
if-else很多,但了解后就很简单很简单,就是插入元素时找到下标位置(也就是算出来的hash值),
没有元素则直接插入
有元素(这就是冲突,hashMap对冲突的解决方法就是链表法,所以hashMap是以数组加链表实现的)就对比key,
key相等 则直接进行value替换后结束,
不相等
且只有一个元素,直接往后插入,
有多个元素的话,
如果是链表就插入到末尾,
如果是红黑树就按红黑树插入逻辑进行插入
(插入链表和红黑数的循环过程也会判断key是否相等,相等则进行value替换后结束),
最后判断是否要扩容。
当然冲突的解决方法也有其他方法,hashMap用的是链表法,
其他注意的方法有:开放定址法、线性探查法、随机探测、线性补偿探测法。
当然你高水平的重写hashCode(),也能尽量避免冲突产生。
扩容resize()
HashMap的容量,默认是16
HashMap的加载因子,默认是0.75
当HashMap中元素数超过容量*加载因子时,HashMap会进行扩容
HashMap扩容的时候,长度会翻倍
hashMap的扩容,需要进行hash的重新分配,耗时耗力尽量避免
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
//如果数组长度达到MAXIMUM_CAPACITY,就不在进行扩容
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//否则如果数组长度大于16小于MAXIMUM_CAPACITY,
//则阈值扩大一倍,注意再判断时也进行了容器的扩大一倍
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
newCap = oldThr;
else { // zero initial threshold signifies using defaults
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;
@SuppressWarnings({"rawtypes","unchecked"})
//创建新数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//循环的把老数组搬到新数组
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
//如果该索引位置只有一个元素,进行重新hash,计算位置,然后插入
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
//如果有多个元素且是红黑数
//那就转换成链表后重新循环
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//如果遇到了链表
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
//内部进行链表的循环,重新hash、放置
do {
next = e.next;
//运算后的hash值进行和oldCap与运算
if ((e.hash & oldCap) == 0) {
//把节点放到原位置的链表里
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
//把节点放到原位置+原数组大小的索引位置的链表里
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
//把链表放到原位置
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
//把链表放到原位置+原数组大小的索引位置
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
看注释应该能理解差不多,我讲讲这个搬运是个神马逻辑:
首先我们看到每次进行搬运都会进行与运算,干嘛用的呢?
其实就是搬运时,会进行hash值与老容量的对比,hash值比老容量小则位置不变,hash值比老容量大,那就得换位置了,新位置=老位置+老容量;
get(key)就不分析了
那我们看看remove(key)是怎么移除的
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
同样进行了hash计算索引位置
final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
//如果第一个元素就是要删的
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
//如果第一个不是要删的而且该元素有下一个节点
else if ((e = p.next) != null) {
//判断是否为红黑树,是就跑到用红黑树逻辑查找
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
//查链表
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
//以上都是查找然后node=找到的元素
//下面开删,链表删除逻辑咱就不讲了
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p)
tab[index] = node.next;
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
咱们就重点看看((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
这句
final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
...
if (root.parent != null)
//找到根节点
root = root.root();
//只看这句,因为用根节点来计算红黑树节点数是否少于6
if (root == null || root.right == null ||
(rl = root.left) == null || rl.left == null) {
tab[index] = first.untreeify(map); // too small
return;
}
...
}
里面做了很多事比如删除,平衡红黑树等,咱就不讲了。我们只看贴出来的一段代码。
因为红黑树是平衡的,所以只需用根节点就可以知道节点总数是否小于6,小于6就执行untreeify(),把树转为链表。
详细关于hashMap的红黑树操作可看
年轻的叔叔:
https://blog.csdn.net/sun112233445/article/details/103350026
到这儿,估计大伙对HashMap有一定理解了把,谢谢大家阅读。