1.底层数据结构
JDK版本不同的数据结构
1.7 数组 + 链表
1.8 数组 + (链表 | 红黑树)
2.添加数据put
- 在添加一个值的时候,首先会计算他的hash码,然后进行二次hash,在对当前长度取模得到在底层数组中的索引位置
- 当取模完成后,会遇到不同元素索引位置相同的情况。我们把这种情况叫做hash冲突,此时会将后一个元素通过链表的形式挂在下边
- 当存储元素数量超过数组容量的四分之三时,会进行扩容,扩容后,也可以减少链表长度。
- 但是如果同一条链上的元素原始hash本就相同,此时通过扩容就不能有减少链表的长度了
3.树化与退化
树化意义
-
红黑树用来避免 DoS 攻击,防止链表超长时性能下降,树化应当是偶然情况,是保底策略
-
hash 表的查找,更新的时间复杂度是 $O(1)$,而红黑树的查找,更新的时间复杂度是 $O(log_2n )$,TreeNode 占用空间也比普通 Node 的大,如非必要,尽量还是使用链表
-
hash 值如果足够随机,则在 hash 表内按泊松分布,在负载因子 0.75 的情况下,长度超过 8 的链表出现概率是 0.00000006,树化阈值选择 8 就是为了让树化几率足够小
-
树化规则
-
当链表长度超过树化阈值 8 时,先尝试扩容来减少链表长度,如果数组容量已经 >=64,才会进行树化
退化规则
-
情况1:在扩容时如果拆分树时,树元素个数 <= 6 则会退化链表
-
情况2:remove 树节点时,若 root、root.left、root.right、root.left.left 有一个为 null ,也会退化为链表(在移除之前检查
4.索引计算
索引计算方法
-
首先,计算对象的 hashCode()
-
再进行调用 HashMap 的 hash() 方法进行二次哈希
-
二次 hash() 是为了综合高位数据,让哈希分布更为均匀
-
-
最后 & (capacity – 1) 得到索引
数组容量为何是 2 的 n 次幂
-
计算索引时效率更高:如果是 2 的 n 次幂可以使用位与运算代替取模
-
扩容时重新计算索引效率更高: hash & oldCap == 0 的元素留在原来位置 ,否则新位置 = 旧位置 + oldCap
注意
-
二次 hash 是为了配合 容量是 2 的 n 次幂 这一设计前提,如果 hash 表的容量不是 2 的 n 次幂,则不必二次 hash
-
容量是 2 的 n 次幂 这一设计计算索引效率更好,但 hash 的分散性就不好,需要二次 hash 来作为补偿,没有采用这一设计的典型例子是 Hashtable
5.put与扩容
put 流程
-
HashMap 是懒惰创建数组的,首次使用才创建数组
-
计算索引(桶下标)
-
如果桶下标还没人占用,创建 Node 占位返回
-
如果桶下标已经有人占用
-
已经是 TreeNode 走红黑树的添加或更新逻辑
-
是普通 Node,走链表的添加或更新逻辑,如果链表长度超过树化阈值,走树化逻辑
-
-
返回前检查容量是否超过阈值,一旦超过进行扩容
1.7 与 1.8 的区别
-
链表插入节点时,1.7 是头插法,1.8 是尾插法
-
1.7 是大于等于阈值且没有空位时才扩容,而 1.8 是大于阈值就扩容
-
1.8 在扩容计算 Node 索引时,会优化
扩容(加载)因子为何默认是 0.75f
当扩容的个数 > 数组长度*负载因子的值
-
在空间占用与查询时间之间取得较好的权衡
-
大于这个值,空间节省了,但链表就会比较长影响性能
-
小于这个值,冲突减少了,但扩容就会更频繁,空间占用也更多
6.多线程下HashMap会有什么问题
- 扩容死链(1.7
- 出现这个问题的主要原因是,在多线程情况下,扩容时,需要把元素从新放入新数组中,那么在同一位置上的元素会顺序放入新数组中,1.7采用的是头插法从而导致了扩容死链问题。
- 数据错乱(1.7 1.8
多个线程同时操作HashMap会出现,数据丢失的情况,是因为在添加元素时,可能在同一位置需要添加多个元素,但是会出现覆盖情况。
7.key 的设计
key 的设计要求
-
HashMap 的 key 可以为 null,但 Map 的其他实现则不然
-
作为 key 的对象,必须实现 hashCode 和 equals,并且 key 的内容不能修改(不可变)如果key是可变的,那么你在HashMap去查询时,它的HashCode就不一样了,也就找不到数据了。
-
key 的 hashCode 应该有良好的散列性
String 对象的 hashCode() 设计
-
目标是达到较为均匀的散列效果,每个字符串的 hashCode 足够独特
-
字符串中的每个字符都可以表现为一个数字,称为 S_i,其中 i 的范围是 0 ~ n - 1
-
散列公式为: S_0∗31^{(n-1)}+ S_1∗31^{(n-2)}+ … S_i ∗ 31^{(n-1-i)}+ …S_{(n-1)}∗31^0
-
31 代入公式有较好的散列特性,并且 31 * h 可以被优化为
-
即 32 ∗h -h
-
即 2^5 ∗h -h
-
即 h≪5 -h
-
8.源码分析
8.1常量
//默认初始容量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大容纳数
//最大容量,如果使用参数的任一构造函数隐式指定了较高的值,则使用该容量。必须是 2 的幂<= 1<<30。
static final int MAXIMUM_CAPACITY = 1 << 30;
//负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//使用树而不是列表的箱计数阈值。将元素添加到至少具有此多个节点的图格时,图格将转换为树。该值必须大于 2,并且应至少为 8,以与树木移除中关于在收缩时转换回普通条柱的假设相吻合。
//阈值 作用于 树化
static final int TREEIFY_THRESHOLD = 8;
//取消树化阈值
static final int UNTREEIFY_THRESHOLD = 6;
//最小树化容量
static final int MIN_TREEIFY_CAPACITY = 64;
在成员变量中可以发现
HashMap定义了默认的初始容量、负载因子、树化阈值、退出树化阈值、最小树化数组的容量以及最大容量
8.2成员变量
//该表在首次使用时初始化,并根据需要调整大小。分配时,长度始终是 2 的幂。(我们还允许在某些操作中使用长度为零,以允许当前不需要的引导机制。
transient Node<K,V>[] table;
//保存缓存的 entrySet()。请注意,AbstractMap 字段用于 keySet() 和 values()。
transient Set<Map.Entry<K,V>> entrySet;
//此映射中包含的键值映射数。
transient int size;
//此 HashMap 在结构上被修改的次数 结构修改是指更改 HashMap 中的映射数量或以其他方式修改其内部结构(例如,重新哈希)的修改。此字段用于使 HashMap 的 Collection-views 上的迭代器快速失败。(请参阅 ConcurrentModificationException)。
transient int modCount;
//要调整大小的下一个大小值(容量 * 负载系数)。
int threshold;
//哈希表的负载因子
final float loadFactor;
8.3构造函数
/**
构造一个具有指定初始容量和负载因子的空 HashMap。
参数:
initialCapacity – 初始容量
loadFactor – 负载系数
抛出: IllegalArgumentException – 如果初始容量为负或负载系数为非正
*/
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);
}
/**
构造一个空的 HashMap,具有指定的初始容量和默认负载系数 (0.75)。
参数: initialCapacity – 初始容量。
抛出:IllegalArgumentException – 如果初始容量为负数。
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//使用默认初始容量 (16) 和默认负载系数 (0.75) 构造一个空的 HashMap。
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // 所有其他字段默认
}
/**
使用与指定 Map 相同的映射构造新的 HashMap。
HashMap 是使用默认负载系数 (0.75) 创建的,初始容量足以在指定的 Map 中保存映射。
参数: m – 其映射将放置在此映射中的映射
Throws: NullPointerException – 如果指定的映射为 null
*/
//参数时一个Map集合的话,就直接添加进去
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
8.4get
/*
返回指定键映射到的值,如果此映射不包含键的映射,则返回 null。
更正式地说,如果此映射包含从键 k 到值 v 的映射,
使得 (key==null ? k==null : key.equals(k)),则此方法返回 v;
否则,它将返回 null。(最多可以有一个这样的映射。
返回值 null 并不一定表示映射不包含键的映射;
映射也有可能将键显式映射到 null。containsKey 操作可用于区分这两种情况。
参见:put(Object, Object)
**/
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
8.5put
//参数为 key,value 的形式
public V put(K key, V value) {
//调用了putVal方法
return putVal(hash(key), key, value, false, true);
}
/*
参数: hash – 键键的哈希值 – 键值 – 要放置的值
onlyIfAbsent – 如果为 true,则不更改现有值
evict – 如果为 false,则表处于创建模式。
这个函数实现了Java中的Map的put方法及其相关方法。
它根据给定的键和值,将键值对添加到Map中。
如果键已存在且onlyIfAbsent为true,则不更改现有的值。
如果evict为false,则表处于创建模式。如果evict为true且表已满,则会清理最久未访问的键值对。
最后,该函数会调整Map的大小并返回旧的值。
**/
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;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
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;
}
}
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;
}
8.6Node
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;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
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;
}
}