HashMap初解
前言
HashMap是Java集合框架中Map接口的一个实现类,它存储键值对,并允许使用键来检索对应的值。HashMap基于哈希表实现,提供了快速的插入、删除和查找操作。它允许空键和空值,并且不保证元素的顺序。在Java中,HashMap是一个非同步的类,因此在多线程环境下需要进行额外的同步处理。
HashMap在JAVA8中使用数据+链表+红黑树实现。
源码解析
构造方法
//无参构造方法 this.loadFactor是哈希表的负载系数。hash表的扩容和当前负载系数*容量有关系
//默认的负载因子为 static final float DEFAULT_LOAD_FACTOR = 0.75f;
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
//带有初始容量的构造函数
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//带有初始容量和负载因子的构造函数
public HashMap(int initialCapacity, float loadFactor) {
//初始容量小于0 抛出异常
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//MAXIMUM_CAPACITY = 1 << 30; 是hash数组的最大值
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
//赋值负载因子 loadFactor);
this.loadFactor = loadFactor;
//hash数组的长度必须为2的n次方
//tableSizeFor(initialCapacity)计算initialCapacity值对应的最小的2的n次方的值
//this.threshold字段在Hash数组初始化之前表示要初始化的数组的长度,在数组初始化之后表示当前数组需要扩容的阈值
this.threshold = tableSizeFor(initialCapacity);
}
//通过移位的方法计算
static final int tableSizeFor(int cap) {
int n = cap - 1;
// 将n的第一个1往后移动1位使n的前两位变为1
n |= n >>> 1;
// 将n的前两个1往后移动2位使n的前四位变为1
n |= n >>> 2;
// 将n的前四个1往后移动4位使n的前8位变为1
n |= n >>> 4;
// 将n的前8个1往后移动8位使n的前16位变为1
n |= n >>> 8;
// 将n的前16个1往后移动16位使n的前32位变为1
n |= n >>> 16;
//n的二进制数全为1 n+1则为 2的幂
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
//带有Map集合的构造函数
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
// 数组桶为空 初始化
if (table == null) { // pre-size
//确保放入这个map不需要进行扩容
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
//要放入的集合的容量(根据负载因子计算的不需要扩容的容量)大于已经初始化的容量 重新计算threshold的值
if (t > threshold)
threshold = tableSizeFor(t);
}
// 要放入的map的数量大于 需要扩容的数量 进行扩容
else if (s > threshold)
resize();
//循环放入
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
总结
- 包含四个构造方法,可以分别传入初始容量、负载因子、Map集合登
- HashMap默认的负载因为为0.75
- HashMap数组长度最大为2的30次方
- HashMap数组长度必须为2的n次方
- this.loadFactor表示负载因子
- this.threshold字段在Hash数组初始化之前表示要初始化的数组的长度,在数组初始化之后表示当前数组需要扩容的阈值
put方法
//放入键值对 返回旧值
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
//计算key的hash值
static final int hash(Object key) {
int h;
//取hash值,并且右移并进行异或运算 (^异或运算)
//将Key的Hash值的高16位与低16位进行异或,在大多数情况下,将key的hash值都参与到计算HashMap的数组桶索引中
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//onlyIfAbsent表示是否是不存在才放入
//evict为false表示是创建模式 和LinkedHashMap相关联
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 数组为空进行初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// (n - 1) & hash 将k的hash与(桶长度-1)按位相与 取hash的后几位 将索引落在桶长度区间内
// 如果当前位置索引为null 直接存入
if ((p = tab[i = (n - 1) & hash]) == null)
//数组中保存的是Node节点
tab[i] = newNode(hash, key, value, null);
//当前数组桶存在节点
else {
Node<K,V> e; K k;
//判断当前节点的 k和要插入的key是否是同一个
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//树操作
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;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// e !=null 表示在链表中存在当前k需要进行修改
// e表示当前要更新的节点
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
//节点访问后钩子函数
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//插入后 长度+1 判断是否到达需要扩容的长度 需要则进行扩容
if (++size > threshold)
resize();
//节点插入后钩子函数
afterNodeInsertion(evict);
return null;
}
总结
- put流程
- 判断当前数组桶是否存在如果不存在则先进行扩容操作
- 根据Key的hash值和桶的长度计算出该key需要在数组同中的位置
- 如果数组桶中对应的位置没有节点则直接放入
- 对应的位置有值判断当前节点key和要放入的key是否相同 调用 == 和equals方法 相同则记录 当前节点为 e
- 不相同则判断当前节点是否为树节点 如果为树节点则代表当前为红黑树调用树节点的put方法 并将返回设为 e
- 不是树节点则代表当前节点为链表,遍历链表查找是否有相同的key如果有 则将节点设为e 如果没有则在链表尾部插入一个新节点,并判断当前链表长度是否需要转化为红黑树。
- 判断节点e是否为空,如果为空则表示是新增的节点,将siez+1并与threshold比较看是否需要扩容并返回null
- e不为空则表示是更新节点 根据onlyIfAbsent参数判断是否需要更新,并返回旧值
- key的hash值是将key的hashcode的低16位是将key的高16位与低16位异或计算出来的
- 数组同长度始终为2的n次方是方便计算取余结果,key落在hash桶中的索引
- 红黑树中的节点为TreeNode
- 数组链表使用尾插法将节点放入
- 链表转化为红黑树的阈值为 TREEIFY_THRESHOLD = 8,当链表长度大于等于8则转化为红黑树
链表转化为红黑树
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) {
//将该hash对应的节点以及链表上的节点 转为树节点 并构建一个双向链表
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);
}
}
总结
- 当一个链表的长度到达需要转化为红黑树的时候 会先判断当前数组桶的长度是否小于64 如果小于64则先进行扩容
- 大于或等于64则需要进行树化
扩容
//返回新的数组桶
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
//为0或者为原来数组的长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 原来的扩容阈值 为0或者为 oldcap对应的2的幂
int oldThr = threshold;
int newCap, newThr = 0;
//进行扩容原来数组有长度 oldCap表示为原来数组桶长度 oldThr为原来的扩容阈值
if (oldCap > 0) {
//原来的容量大于等于最大容量 不进行操作
if (oldCap >= MAXIMUM_CAPACITY) {
//扩容阈值变为Integer.MAX_VALUE
threshold = Integer.MAX_VALUE;
return oldTab;
}
//新容量扩大为原原来容量的2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//扩容阈值也变为原来的两倍
newThr = oldThr << 1; // double threshold
}
//原来数组没有长度 但初始化是传入了容量 传入初始容量调用tableSizeFor方法初始了threshold 对应有initialCapacity参数的两个构造方法
else if (oldThr > 0) // initial capacity was placed in threshold
//新容量等于tableSizeFor方法初始后的threshold
newCap = oldThr;
//原来数组没有长度 空的hashmap
else { // zero initial threshold signifies using defaults
//默认的容量 16
newCap = DEFAULT_INITIAL_CAPACITY;
// threshold 负载因子*16(默认容量)
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 = cap * loadFactor 当cap >= MAXIMUM_CAPACITY threshold = 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;
//当前hashcode值对应的数只有一个,在数组中
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
// 如果e 元素是链表类型,则需要遍历链表,将其一拆为二
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
//将节点的hash与原容量做按位与
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;
}
总结
- 扩容中新的数组桶长度与新的扩容阈值计算流程
- oldCap有值表示原来的数组桶有值进行扩容
- 判断oldCap是否大于等于最大的数组长度如果等于则将threshold 扩容阈值设置为Integer.MAX_VALUE直接返回oldTab
- oldCap不大于最大的数组长度 则newCap = oldCap * 2 、如果newCap <MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY newThr = oldThr*2 否则newThr 还是0
- 否则就是原数组桶没有值oldThr > 0表示在构造函数初始化了数组桶长度 newCap = oldThr newThr = 0
- 原数组桶没有值并且构造函数也没有初始化数组长度 newCap = 16 、 newThr = (int)(0.75 * 16);
- 如果newThr = 0则使用newCap与负载因子计算newThr
- 如果newCap小于数组最大值并且计算后的thr也小于数组最大值则为计算后的值
- 否则为Integer.MAX_VALUE
- 计算结束后将threshold赋值为newThr
- oldCap有值表示原来的数组桶有值进行扩容
- 以newCap为大小创建一个数组
- 如果原数组不为空则将原数组的数据转移到新数组,遍历原数组桶的每个节点
- 当前节点只有一个node(node的next == null)
- 则使用key的hash值与(newCap-1)做与操作重新计算索引
- 当前节点为树节点
- 调用树节点的拆分,将一个红黑树拆成两个分别放入新数组的不同位置,与链表拆分相似,红黑树有一个判断是否要从红黑树变为链表的逻辑
- 当前节点为链表节点,将链表拆成低位链表和高位链表(例如原数组长度为1000则key索引位置的只与hash值的后三位有关系,扩容后则与后四位有关系)
- 将节点的hash与oldCap(二进制为100000)进行按位与,取e的hash值与oldCap对应的1位的值
- 如果结果位0则将节点放入低位链表,否则放入高位链表
- 低位链表放入newTab[j]处(hash值倒数第四位为0则索引位置还是j)、高链表放入newTab[j + oldCap]处(hash值倒数第四位为1)
- 当前节点只有一个node(node的next == null)
- 红黑树退化为链表的阈值为UNTREEIFY_THRESHOLD = 6
get方法
根据Key查找对应的value
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
流程
- 如果数组为空或者长度为0,或者根据key的hash和数组桶的长度定位到数组桶对应的索引 节点 为null 则返回null
- 判断该节点的key与要查找的key是否相同,== 或 equals方法 如果相同则返回对应value
- 如果当前节点的next不为空则去红黑树或者链表中查找,找到后返回找不到返回null
remove方法
public boolean remove(Object key, Object value) {
return removeNode(hash(key), key, value, true, true) != null;
}
//matchValue – 如果为 true,则仅当值相等时删除(区分是按key删除 还是 k,v删除)
//movable – 如果为 false,则在删除时不要移动其他节点(和树相关)
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;
//数组桶不为空 并且 长度大于0 并且要删除的key对应的位置不为空
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)
//如果是树节点则在树中获取要删除的key
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
//链表中查找要删除的key
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
//保存p为要删除的节点的上一个
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;
}
LinkedHashMap
LinkedHashMap继承了HashMap,实现了Map接口,LinkedHashMap与HashMap保存的数据结构相同,只不过LinkedHashMap节点是另一个节点有一个前后指针,在每次操作完之后都会维护这个节点的前后指针,形成一个链表,保证插入顺序
Hash中包含下面几个函数
// 新建一个节点
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
return new Node<>(hash, key, value, next);
}
// 替换一个节点
Node<K,V> replacementNode(Node<K,V> p, Node<K,V> next) {
return new Node<>(p.hash, p.key, p.value, next);
}
// 创建一个树节点
TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) {
return new TreeNode<>(hash, key, value, next);
}
// 替换树节点
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
return new TreeNode<>(p.hash, p.key, p.value, next);
}
// 节点被访问后调用的方法
void afterNodeAccess(Node<K,V> p) { }
//节点被插入后调用的方法
void afterNodeInsertion(boolean evict) { }
//节点被移除后调用的方法
void afterNodeRemoval(Node<K,V> p) { }
LinkedHashMap重写以上的几个方法来实现自己的逻辑
//双向链表中的头节点
transient LinkedHashMap.Entry<K,V> head;
/**
* 双向链表中的尾节点
*/
transient LinkedHashMap.Entry<K,V> tail;
static class Entry<K,V> extends HashMap.Node<K,V> {
// 新增由自己的双向链表维护的前后指针
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
// 重写了hashmap 新建节点的方法
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
LinkedHashMap.Entry<K,V> p =
new LinkedHashMap.Entry<K,V>(hash, key, value, e);
//新节点默认插在最后
linkNodeLast(p);
return p;
}
// 替换节点
Node<K,V> replacementNode(Node<K,V> p, Node<K,V> next) {
LinkedHashMap.Entry<K,V> q = (LinkedHashMap.Entry<K,V>)p;
LinkedHashMap.Entry<K,V> t =
new LinkedHashMap.Entry<K,V>(q.hash, q.key, q.value, next);
transferLinks(q, t);
return t;
}
// 新建树节点
TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) {
TreeNode<K,V> p = new TreeNode<K,V>(hash, key, value, next);
linkNodeLast(p);
return p;
}
// 替换树节点
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
LinkedHashMap.Entry<K,V> q = (LinkedHashMap.Entry<K,V>)p;
TreeNode<K,V> t = new TreeNode<K,V>(q.hash, q.key, q.value, next);
transferLinks(q, t);
return t;
}
// 节点移除后 hashmap的钩子函数
void afterNodeRemoval(Node<K,V> e) { // unlink
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
p.before = p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a == null)
tail = b;
else
a.before = b;
}
// 插入新节点之后
// evict 为FALSE表示创建模式
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
//removeEldestEntry删除最头部的节点 用于固定数目的缓存 当插入新节点后需要将最近未被访问的节点删除
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
//节点访问后
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMap.Entry<K,V> last;
// 如果是按访问顺序 并且最后一个节点不是当前节点 将最近访问的这个节点放在尾部
if (accessOrder && (last = tail) != e) {
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a != null)
a.before = b;
else
last = b;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
tail = p;
++modCount;
}
}
//重写父类的方法 添加访问方法
public V get(Object key) {
Node<K,V> e;
// 查找调用 父类HashMap的方法 时间复杂度为O(1)
if ((e = getNode(hash(key), key)) == null)
return null;
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
/**
* //重写父类的方法 添加访问方法
*/
public V getOrDefault(Object key, V defaultValue) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return defaultValue;
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
总结
- LinkedHashMap中的节点为自己的内部类Entry,该类新增了Entry<K,V> before, after两个字段维护双向链表
- LinkedHashMap类中保存了双向链表的头节点和尾节点,用于维护双向链表
- LinkedHashMap重写了newNode方法每次新建节点会将该节点放到双向链表的尾部
- replacementNode、newTreeNode、replacementTreeNode的重写与3功能类似,都是为了维护双向链表
- afterNodeRemoval表示在节点删除后,解除该节点在双向链表中的关联
- 在LinkedHashMap中有一个 final boolean accessOrder; 属性可用于实现LRU缓存 。当accessOrder为true时每次访问都会将此次访问的节点放在双向链表的队尾
- 调用 put, putIfAbsent, get, getOrDefault, compute, computeIfAbsent, computeIfPresent, or merge方法将导致对相应条目的访问(假设它在调用完成后存在)。仅当替换值时,这些 replace方法才会导致对条目的访问。该 putAll方法为指定映射中的每个映射生成一个条目访问,其顺序是键值映射由指定映射的条目集迭代器提供。没有其他方法生成条目访问。具体而言,对集合视图的操作不会影响后备映射的迭代顺序。
- 在每次afterNodeInsertion方法调用 removeEldestEntry(first)方法来判断是否需要删除头节点 默认为false
- 当accessOrder设为true ,重写removeEldestEntry(first)方法可以实现自己逻辑的LRU缓存