目录
注:阅读本文之前,需要对散列表有一定基础,移步此文 哈希 / 散列表详细介绍
一、HashMap 散列表
什么是散列表
散列表(Hash Table):也被称为哈希表,是一种常见的数据结构,用于存储键值对。散列表是基于散列思想实现的Map数据结构。
将散列思想应用到散列表数据结构上,就是通过hash函数提取键值对中的Key的散列值,再将键值对映射到固定的数组下标中,利用数组支持随机访问的特性,实现O(1)时间的存储和查询。
不过一般不会使用hash函数计算后的散列值作为数组下标,因为Java中Object#hashCode()
返回的散列值是int类型,值域是2^32,我们不可能创建如此大的数组。最简单的做法就是将散列值对数组长度取余后再去绝对值。
HashMap的底层结构
HashMap
是基于分离链表法解决散列冲突的动态散列表。
在Java7中使用的是数组 + 链表,发生散列冲突的键值对会用头插法添加到单链表中;
在Java8中使用的是数组 + 链表 + 红黑树,发生散列冲突的键值对会用尾插法添加到单链表中。如果链表的长度大于8且散列表的容量大于64,则会将链表树化为红黑树。当扩容再散列时,如果红黑树的长度低于6,则会还原为链表。
二、源码解读
注:不同jdk版本的源码会有一定差异,不过大致上是相同的,我使用的是 openjdk version “1.8.0_342”。
类图
-
HashMap
实现了Cloneable
,可以被克隆。 -
HashMap
实现了Serializable
,可以被序列化。 -
HashMap
继承自AbstractMap
,实现了Map接口,具有Map的所有功能。
成员变量
/**
* 默认的初始容量为16
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
/**
* 最大的容量为2的30次方
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 默认的装载因子
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 树化阈值(当一个桶中的元素个数大于等于8时进行树化)
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 还原阈值(当一个桶中的元素个数小于等于6时把树转化为链表)
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* 树化最小容量(Java 8新增)
* 只有当整个散列表的长度满足最小容量的要求时才允许链表树化,否则直接扩容,而不是树化
*/
static final int MIN_TREEIFY_CAPACITY = 64;
/**
* 底层数组,又叫作桶(每个元素是一个单链表或红黑树)
*/
transient Node<K,V>[] table;
/**
* 作为entrySet()返回值的缓存
*/
transient Set<Map.Entry<K,V>> entrySet;
/**
* 有效键值对的数量
*/
transient int size;
/**
* 修改次数,用于在迭代的时候执行快速失败策略
*/
transient int modCount;
/**
* 扩容阈值
* 当桶的使用数量达到多少时进行扩容,threshold = capacity * loadFactor(容量 * 装载因子)
*/
int threshold;
/**
* 装载因子上限
*/
final float loadFactor;
HashMap 内部类
Node内部类
/**
* 链表节点
*/
static class Node<K,V> implements Map.Entry<K,V> {
// 哈希/散列值(相同链表上的key的哈希值可能相同)
final int hash;
// Key
final K key;
// Value
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节点 的 hashCode 取 Key 和 Value 的 hashCode
public final int hashCode() {
/**
* ^ 是Java中 按位异或运算符
* 如果两个操作数的某一位都是1或都是0,则结果的该位为0。
* 如果两个操作数的某一位一个是1,另一个是0,则结果的该位为1。
* 1010 ^ 1100 = 0110
* 使用异或运算符是为了增加哈希值的差异性,能更好分散数据,提高散列表数据结构的性能
*/
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
// 两个 Node 的 Key 和 Value 都相等,才认为相等
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
TreeNode内部类
// 红黑树节点(Java 8 新增)
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
// 父节点
TreeNode<K,V> parent;
// 左节点
TreeNode<K,V> left;
// 右节点
TreeNode<K,V> right;
// 删除辅助节点(用于在删除的时候可以快速找到它的前置节点)
TreeNode<K,V> prev;
// 颜色
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
// 返回树的根节点
final TreeNode<K,V> root() {
for (TreeNode<K,V> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
// 后续一些辅助函数就不一一解读,在核心源码中遇到的时候再细谈.......
}
构造方法
HashMap中提供了4个构造方法:
public HashMap()
:无参构造方法,设置默认装载因子为0.75public HashMap(int initialCapacity)
:构造一个具体指定初始化容量的HashMappublic HashMap(int initialCapacity, float loadFactor)
:构造一个具有指定容量和负载因子的HashMappublic HashMap(Map<? extends K, ? extends V> m)
:构造一个包含此m所有键值对的HashMap,设置默认装载因子为0.75
第一种构造函数 public HashMap()
:
无参构造方法,设置默认装载因子为0.75
public HashMap() {
// 设置装载因子 0.75
// static final float DEFAULT_LOAD_FACTOR = 0.75f;
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
第二种构造函数 public HashMap(int initialCapacity)
:
构造一个具体指定初始化容量的HashMap
// 装载因子设置为 DEFAULT_LOAD_FACTOR 0.75
public HashMap(int initialCapacity) {
// this 调用的是第三种构造函数
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
第三种构造函数 public HashMap(int initialCapacity, float loadFactor)
:
构造一个具有指定容量和负载因子的HashMap
public HashMap(int initialCapacity, float loadFactor) {
// 校验初始容量
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
// 最大容量限制,如果大于最大容量,则限制为最大容量
// static final int MAXIMUM_CAPACITY = 1 << 30;
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// 校验装载因子的合法性
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
this.loadFactor = loadFactor;
// 计算扩容阈值(返回给定目标容量的2次幂大小 5-->8 10-->16)
this.threshold = tableSizeFor(initialCapacity);
}
第四种构造函数 public HashMap(Map<? extends K, ? extends V> m)
:
构造一个包含此m所有键值对的HashMap,设置默认装载因子为0.75,
public HashMap(Map<? extends K, ? extends V> m) {
// 设置装载因子 0.75
this.loadFactor = DEFAULT_LOAD_FACTOR;
// 批量添加(下文添加方法中详细介绍此方法)
putMapEntries(m, false);
}
添加方法
HashMap中主要提供了3个与添加键值对相关的方法:
1、public V put(K key, V value)
源码如下:
public V put(K key, V value) {
// putVal方法
return putVal(hash(key), key, value, false, true);
}
2、public V putIfAbsent(K key, V value)
源码如下:
public V putIfAbsent(K key, V value) {
// putVal方法
return putVal(hash(key), key, value, true, true);
}
3、public V putIfAbsent(K key, V value)
源码如下:
public void putAll(Map<? extends K, ? extends V> m) {
putMapEntries(m, true);
}
// 批量添加或更新键值对
// evict:是否驱逐最早的节点(在 LinkedHashMap 中使用,HashMap忽略)
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
// 如果传入map的长度 大于0
if (s > 0) {
// 如果数组为空
if (table == null) {
// 计算map的容量,键值对的数量 = 容量 * 装载因子
// float ft容量 = 键值对的数量 / 装载因子
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
// 如果容量大于阈值,则重新计算扩容阈值
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方法
putVal(hash(key), key, value, false, evict);
}
}
}
不管是逐个添加或者批量添加,最终都会是通过hash(key)
计算出Key的散列值,再通过putVal
方法添加键值对。
以上三个添加方法都离不开putVal
以及hash(key)
,都是直接或间接调用了两个方法,接下来我们详细解读以上提到的两个方法。
hash(key)
:计算散列值
上文中有提很多次一个很重要:使用Hash函数计算键值对中Key的散列值。
Hash函数是散列表的核心,Hash函数是否足够随机,会直接影响散列表的查询性能。
// 1 次位运算 + 1次异或运算
static final int hash(Object key) {
int h;
/**
* 如果 key == null 返回 0
* 反之:h = key.hashCode()
* return h ^ (h >>> 16)
* 也就是: h 异或 (h 向右移16位)
*/
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
阅读到这里的小伙伴可以跳到文末:“加餐1” 后,再继续阅读全文
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)
:添加键值对
// onlyIfAbsent:如果为 true,不会覆盖旧值
// evict:是否驱逐最早的节点(在 LinkedHashMap 中使用,HashMap忽略)
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab;
Node<K,V> p;
// n 数组长度
int n;
int i;
// 1、第一步
// 如果数组为空,则扩容
// resize()方法后文会解读
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 2、第二步
// (n - 1) & hash 散列值取余 映射到数组下标 i
// if(p == null) 如果数组下标位置处为空 则创建并插入节点
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 3、第三步
// 如果数组下标位置处不为空
// 表示发生了散列值冲突,需要插入链表或红黑树
else {
Node<K,V> e;
K k;
/**
* 3.1 分解if中的条件判断
* p.hash == hash (此下标处的节点散列值 == 插入节点的散列值)
* &&
* ((k = p.key) == key || (key != null && key.equals(k)))
* 继续分解
* (k = p.key) == key (k节点上的key == 插入节点的key)
* ||
* (key != null && key.equals(k)) (判断 k 和 key 是否相等)
*
*
* 如果桶中第一个元素的key与待插入元素的key相同,也就是说,如果待插入的key在链表中找到了,保存到e中用于后续修改value值
*/
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 3.2 如果此节点是红黑树节点 则采用红黑树的插入方式
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 3.3 如果是链表结构 则采用链表的插入方式
else {
// 3.3.1 遍历链表
for (int binCount = 0; ; ++binCount) {
// 3.3.2 如果下一个节点为null
if ((e = p.next) == null) {
// 3.3.3 创建一个新节点并插入
p.next = newNode(hash, key, value, null);
// 3.3.4 如果 链表节点超过了树化阈值,则将链表转化为红黑树
// 3.3.5 因为第一个越苏没有添加到binCount中,因此这里-1
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
// 3.3.6 如果待插入的key在链表中找到了,则退出循环(上文有出现类似判断)
// 3.3.7 也就是说 如果桶中第一个元素的key与待插入元素的key相同,保存到e中用于后续修改value值
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 3.4 如果找到了等于Key的元素,则e != null 新 Value 替换旧 Value
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
// 访问节点回调(用于 LinkedHashMap,默认为空实现)
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 如果键值对数量大于阈值 则触发扩容机制
if (++size > threshold)
resize();
// 新增节点回调(用于 LinkedHashMap,默认为空实现)
afterNodeInsertion(evict);
return null;
}
总结为以下步骤:
- 1、校验数组是否为空,如果为空 则触发扩容机制(说明数组的创建时机 是在首次添加键值对时 )
- 2、根据散列值取余计算出数组下标(
(n - 1) & hash
),再判断数组下标处是否有值(也就是判断是否散列冲突) - 3、如果没有散列冲突,则创建新节点并插入,跳到第9步
- 4、如果有散列冲突,则再判断此下标处键值对的Key同本次插入的键值对的Key是否相等(也就是校验Key是否相等,保证Key的唯一性)
- 5、如果相等,直接跳到第8步。
- 6、如果不等,则判断此下标处的桶是链表结构or红黑树
- 7、如果是链表结构 则采用链表的插入方式;如果是红黑树 则采用红黑树的插入方式。
- 8、如果找到了等于本次插入键值对的Key,则替换掉原来的Value
- 9、如果数组下标位置处不为空,也就是走第3步后,才会走到这里;对数组数量+1操作,且校验是否扩容。
继续解读:
在putValu()
方法中,如果添加键值对后,数组长度超过扩容阈值,就会调用resize()
方法扩容。
final Node<K,V>[] resize() {
// 定义 旧数组 变量
Node<K,V>[] oldTab = table;
// 如果数组为 null 则旧容量置为0
// 如数组不为 null 则旧容量为置为 数组的长度(划重点:数组的长度)
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 定义 旧扩容阈值 变量
int oldThr = threshold;
// 定义 新容量 新扩容阈值 变量为 0
int newCap, newThr = 0;
// 1、第一步
// 如果旧容量 > 0(表示不是第一次添加元素,数组里面有元素)
if (oldCap > 0) {
// 极端情况:
// 如果旧容量 >= 最大容量,则此时无法扩容,将扩容阈值设置为整数最大值,直接返回旧容量
// static final int MAXIMUM_CAPACITY = 1 << 30;
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 普通情况:
// oldCap << 1 旧容量扩容为原来的两倍
// (新容量 < 最大容量) 且 (旧容量 >= 默认容量)
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 扩容阈值扩大为原来的两倍
newThr = oldThr << 1; // double threshold
}
// 使用非无参构造方法创建的map,第一次插入元素会走到这里
else if (oldThr > 0)
// 初始化容量 置为 扩容阈值
newCap = oldThr;
// 调用无参构造方法创建的map,第一次插入元素会走到这里
else {
// static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 新容量 默认为 16
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;
// 2、第二步
// 使用新容量 创建新数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
// 如果旧数组不等于 null,则将旧数组上的键值对 再散列到新数组上
if (oldTab != null) {
// 遍历旧数组上的每个桶
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
// 如果此下标处的桶不为null
if ((e = oldTab[j]) != null) {
// 传递给 e 后,置为空
oldTab[j] = null;
// 如果这个桶中只有一个元素
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;
do {
next = e.next;
// 如果元素的哈希值与旧数组长度的按位与运算结果为0,将元素添加到低位链表中。
// 如果低位链表为空,将该元素作为链表的头节点,否则将该元素添加到低位链表的尾部。
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
// 如果元素的哈希值与旧数组长度的按位与运算结果不为0,将元素添加到高位链表中。
// 如果高位链表为空,将该元素作为链表的头节点,否则将该元素添加到高位链表的尾部。
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;
}
观后感:
大师级代码,写jdk的都是些什么妖魔鬼怪。
膜拜膜拜 !
我啥时候能写出这种水平的代码?
他们有几个脑子?
也许是我太菜了 感觉可读性一般
算了 人家可能也没考虑到 国内卷到读源码 [狗头保命]
不多bb了 继续
获取方法
说了以上这么大一段添加方法,看完的小伙伴先给自己鼓鼓掌
获取方法相对于添加方法来说逻辑简单很多,源码如下:
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
继续解读getNode(hash(key), key)
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 首先是:(tab = table) != null && (n = tab.length) > 0 校验table是否为空
// 再者是:(first = tab[(n - 1) & hash]) != null) 计算hash值映射到下标处的桶是否为空
// 其一为空 则直接返回null
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;
// 如果下一个节点不为null
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;
}
移出方法
源码如下:
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value;
}
继续解读removeNode(hash(key), key, null, false, true)
// matchValue:用于指示是否需要匹配节点的值。如果为 true,则在移除节点之前,需要将 value 参数与节点的值进行匹配;如果为 false,则无需进行值的匹配。
// movable:用于指示是否可以移动其他节点来填补被移除节点的位置。如果为 true,则在移除节点后,可以移动其他节点以填补空缺;如果为 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;
// 同getNode中的第一个if类似
// 校验table是否为空 校验hash值映射到数组中的桶是否为空,其一为空 直接返回null
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;
// p是根节点 先校验根节点
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
// 如果下一个节点不为null
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;
// 删除节点回调(用于 LinkedHashMap,默认为空实现)
afterNodeRemoval(node);
return node;
}
}
return null;
}
加餐1:为什么HashMap要在Object#hashCode()
方法计算的散列值上做增加扰动(也就是为什么要去做位移和异或运算),而不是直接使用Object#hashCode()
得到的值?
因为由于数组的长度有限,在将散列值映射到数组下标的时候,会使用数组的长度做取余运算,也就意味着最终影响下标位置的只有散列值低几位。因此HashMap会对散列值做位移和异或运算,让高16位和低16位做异或运算,等于说是在低位中加入了高位的特性,这样能让高位的数值也会影响到数组下标的计算。
加餐2:说一下HashMap
的底层结构
HashMap
是基于分离链表法解决散列冲突的动态散列表
- 在jdk7中,使用的是“
数组 + 链表
”,发生散列冲突的时候键值对会用头插法添加到单链表中 - 在jdk8中,使用的是“
数组 + 链表 + 红黑树
”,发生散列冲突的时候会使用尾插法添加到单链表中。如果链表的长度大于8且散列表容量大于64的时候,会将链表树化为红黑树。在扩容再散列时,如果红黑树的长度低于6则会还原为链表。
HashMap
的数组长度保证是2的整数次幂,且默认数组容量是16,默认装载因子是16,默认装载因子是0.75,扩容阈值是12。且hashmap的 key 和 value 都支持null
,key为null的键值对会直接映射到数组下表为0的桶中(因为null的hash值是0,取余映射到数组下标后也是table[0]的桶)。
加餐3:为什么HashMap
采用分离链表法而不是开放寻址法
- 首先:
HashMap
的定位是一个相对通用型的散列表容器,它应该在面对各种输入场景中都表现稳定。 - 其次:开放寻址法的散列冲突发生的概率天然比分离链表法高,在存储相同的数据量时,开放寻址法需要预先申请更大的数组空间,内存利用率也不会很高,因此开放寻址法更适合在小数据量且装载因子较小的场景下使用
- 再者:分离链表法对装载因子的容忍程度更高,能够适合大数据量,内存利用率也更高。
- 最后:
ThreadLocal
的数据结构就是一个使用开放寻址法的散列表,因为项目中不会大量使用ThreadLocal
线程局部存储,所以它是一个小规模数据场景,这里使用开放寻址法是没问题的。
加餐4:为什么HashMap
在java8中引入红黑树?且为什么不直接使用“数组 + 红黑树”
Q1:当散列冲突加剧的时候,在链表中寻找对应的元素的时间复杂度是 O(K),K 是链表长度。而红黑树是查找的时间复杂度是O(lgK),最坏情况下时间复杂度是O(lgn),时间复杂度比链表更低
Q2:红黑树是兜底策略,而不一定是最优策略。因为维护红黑树节点的内存性能消耗是比链表大,因此当桶的节点数很低的时候,并不能体现出红黑树的优势,反而是链表更具有性价比
加餐5:如何保证HashMap
的线程安全?
- 使用
HashTable
容器类,它是线程安全版本的散列表,在所有方法上增加synchronized
关键字,且不支持null作为key
Map<String, String> hashtable = new Hashtable<>();
- 使用
Collections.synchronizedMap
包装类,原理也是在所有方法上增加synchronized
关键字
Map<String, String> synchronizedHashMap = Collections.synchronizedMap(new HashMap<String, String>());
- 使用
ConcurrentHashMap
容器类,它是基于CAS无锁 + 分段实现的线程安全散列表
Map<String, String> concurrentHashMap = new ConcurrentHashMap<>();
加餐6:仔细读题
用HashMap
存1万条数据,在构造时传1万容量,当添加到1万数据时会触发扩容吗?
用HashMap
存1千条数据,在构造时传1千容量,当添加到1千数据时会触发扩容吗?
考点:对于HashMap
容量和扩容阈值的理解
在构造器传入的参数initialCapacity
并不一定是最终容量,HashMap
会使用tableSizeFor()
方法计算出一个最接近2的整数幂的值,而扩容阈值是在此基础上乘以0.75装载因子
1万: 10000 转最近的 2 的整数幂是 16384,再乘以装载因子上限得出扩容阈值为 12288,所以不会触发扩容;
1千: 1000 转最近的 2 的整数幂是 1024,再乘以装载因子上限得出扩容阈值为 768,所以会触发扩容;