jdk 源码系列之 HashMap

用户头像
苏格兰、情调
关注
发布于: 4 小时前
jdk 源码系列之HashMap

jdk 源码系列之 HashMap



JDK 源码系列





前言



了解 HashMap,理解扩容机制、数据结构、阈值以及初始化机制,对使用、优化等有所裨益。



继承关系



1
2
3
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
}

从这段代码,我们可知,HashMap 继承 AbstractMap 同时实现 Map 类。换句话说,Map 作为顶级父类,只定义方法,实现由子类实现,还继承 AbstractMap 那也是说,也可以调用 AbstractMap 里面的方法。



这里先摁住,还不清楚 HashMap 调了 AbstractMap 里面哪些方法。



HashMap



点进去先看看是数据结构是什么。



数据结构



/**
* Basic hash bin node, used for most entries. (See below for
* TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
*/
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;
}
}

向上继承 Map.Entry<K,V> 接口,并实现了 getKey getValue toString hashCode setValue equals 这六个方法。之后自己在实现一个链表 Node 的内部类,这个链表的节点都保存三个东西,hash key value 这三个变量。



接下来,我们从创建一个 HashMap 入手。



创建 HashMap



通常来讲,HashMap 有三种 new 的方式。



Map<String, String> first = new HashMap<>();
Map<String, String> second = new HashMap<>(12);
Map<String, String> third = new HashMap<>(first);
Map<String, String> fourth = new HashMap<>(12 , 1);

第一种是无参构造,第二种是传入一个 int 值,第三种是允许传入一个 Map 实现的任何子类,第四种是两个 int 类型。



/**
* Constructs an empty <tt>HashMap</tt> with the default initial capacity
* (16) and the default load factor (0.75).
*/
// first 的构造
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
/**
* Constructs an empty <tt>HashMap</tt> with the specified initial
* capacity and the default load factor (0.75).
*
* @param initialCapacity the initial capacity.
* @throws IllegalArgumentException if the initial capacity is negative.
*/
// second 的构造
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/**
* Constructs a new <tt>HashMap</tt> with the same mappings as the
* specified <tt>Map</tt>. The <tt>HashMap</tt> is created with
* default load factor (0.75) and an initial capacity sufficient to
* hold the mappings in the specified <tt>Map</tt>.
*
* @param m the map whose mappings are to be placed in this map
* @throws NullPointerException if the specified map is null
*/
// third 的构造
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
/**
* Constructs an empty <tt>HashMap</tt> with the specified initial
* capacity and load factor.
*
* @param initialCapacity the initial capacity
* @param loadFactor the load factor
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor is nonpositive
*/
// fourth 的构造
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);
}

这里提到三个变量,分别 initialCapacityloadFactorthreshold



看中文意思,分别是初始化容量、负载因子、阈值。其中负载因子,也就是 loadFactor 默认是 0.75f



/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;

在 third 这个构造函数里面,有个 putMapEntries 的方法。



/**
* Implements Map.putAll and Map constructor.
* 构造方法的时候就全部添加进去 hashMap
* @param m the map 传入的Map
* @param evict false when initially constructing this map, else
* true (relayed to method afterNodeInsertion). 构造使用到就是 false 非构造就是 true
*/
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
// Map 的大小
int s = m.size();
if (s > 0) {
// 由于是构造调用的,table = null
if (table == null) { // pre-size
// 在调用次方法的时候,先赋值了 loadFactor = 0.75。所以ft = (Map 长度 / 0.75) + 1.0
float ft = ((float)s / loadFactor) + 1.0F;
// 判断是否超出 MAXIMUM_CAPACITY = 1 << 30 也就是 2 的30次幂大小
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
// 起始 threshold = 0,这个也在 fourth 的构造里面使用到了,下面有解释
if (t > threshold)
// 如果 t 的大小在 2 的(n-1)次幂 与 2 的 n 次幂之间,则 t 的大小为 2 的n次幂。不懂没关系,下面有解释、举例
threshold = tableSizeFor(t);
}
// Map的长度 是否 大于 刚刚 tableSizeFor 之后的长度
// 首次是不会大于这个值的
else if (s > threshold)
// 扩容,后面对这个方法讲解
resize();
// 将 Map 的子类对象 放进HashMap
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
// 先通过位运算减少 key 的冲突,在放进去
putVal(hash(key), key, value, false, evict);
}
}
}

putMapEntries 这个方法就是 HashMap 在构造的时候,放进 Map 的对象,那么就会将他是所有 k v 都放进去,然后初始化长度,这个长度是当前 Map 长度靠近 2 n次幂 长度。比如



  • Map 7,则HashMap 初始化8,

  • 如果是12 则是 16,

  • 如果是16 则是 16.



之后判断是否需要扩容,由于是首次,所有不需要,接着就是放进 HashMap。先通过位运算计算 Hash 减少冲突,在pull。



/**
* Computes key.hashCode() and spreads (XORs) higher bits of hash
* to lower. Because the table uses power-of-two masking, sets of
* hashes that vary only in bits above the current mask will
* always collide. (Among known examples are sets of Float keys
* holding consecutive whole numbers in small tables.) So we
* apply a transform that spreads the impact of higher bits
* downward. There is a tradeoff between speed, utility, and
* quality of bit-spreading. Because many common sets of hashes
* are already reasonably distributed (so don't benefit from
* spreading), and because we use trees to handle large sets of
* collisions in bins, we just XOR some shifted bits in the
* cheapest possible way to reduce systematic lossage, as well as
* to incorporate impact of the highest bits that would otherwise
* never be used in index calculations because of table bounds.
*
* 通过位运算减少 Hash 冲突
*/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

在 fourth 这个构造函数里面,还有一个这 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;
}

进行了位运算,由于前面好保证了 cap 非负数。



  • »> 右移,高位补零。比如 0001,使用 »> 1 之后就是 00001,对其长度,则1去掉。

  • **** 或操作,不同取1,相同1取1,相同0取0



tableSizeFor 方法里面的意思就是,如果 cap 介于 2 (n - 1)次幂 与2 n次幂之间,cap 的值为 2 n次幂 - 1 大小。



int n = 18;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
System.err.println(n); // 31
int n = 16;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
System.err.println(n); // 31
int n = 14;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
System.err.println(n); // 15

最后只要算的 n 非负数,且不大于 int 类型最大值,则 n + 1。也就是说这个初始容量,无论设置什么数字,都会是 2 的倍数。



HashMap 添加



我们常常使用 kv 的形式存储。



map.put("sin", "sy");

点进去看看,HashMap 如何存储。



// 首先来到父类定义的
V put(K key, V value);
/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
* hashMap 的添加
*/
// 然后实现父类定义的方法
public V put(K key, V value) {
// hash 方法上面有提到作用
return putVal(hash(key), key, value, false, true);
}

pull value 的时候,传入先经过位运算的 key 的 hashCode,然后传 key value、false,true



/**
* Implements Map.put and related methods.
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value 这个值在pull 的时候是false
* @param evict if false, the table is in creation mode. 这个值在pull 的时候是 true
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
// 初始化两个容器 一个 k v 数组链表、一个是链表
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 第一次这里是null,所以 n = (tab = resize()).length;
if ((tab = table) == null || (n = tab.length) == 0)
// resize 下面有讲解
// 是否扩容处理,没有扩容,则是当前长度,有则是之前的两倍
n = (tab = resize()).length;
// 因为数组从0开始计算,所以是 n - 1 取模,确定值放置在数值链表位置
// 如果当前数组链表为null,同时 p = 链表table
if ((p = tab[i = (n - 1) & hash]) == null)
// 创建一个链表
// 没有key值,就给你创建一个
tab[i] = newNode(hash, key, value, null);
// 有值的情况下
else {
// 数组链表头有数据
// 初始化 链表 泛型k
Node<K,V> e; K k;
// hash 值一致且key值的地址也一样 或者 key不为null 且key的值相同
// 大概就是相同key,替换value
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 如果 p 是 树结构
else if (p instanceof TreeNode)
// 按树的形式 pul 进去
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 值不相同,也不是树结构,且链表头还相同,同一节点底下还有链表
else {
// 向下遍历
for (int binCount = 0; ; ++binCount) {
// 链表下一节点为null
if ((e = p.next) == null) {
// p的下一节点指向创建一个新的链表
p.next = newNode(hash, key, value, null);
// 当链表的长度大于等于7,因为从1开始,也是说链表头有值,所以才从一开始。另外这是 ++1 性能比 i++好,i++比起++i多了一个创建临时变量(放入数据栈)的步骤,因此效率低一些
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;
}
}
// 当链表不为null,建立映射,kv
if (e != null) { // existing mapping for key
V oldValue = e.value;
// onlyIfAbsent 如果是 true 不改变现有的值,false 改变,或者 oldValue == null
if (!onlyIfAbsent || oldValue == null)
// 完成关联
e.value = value;
// 尾插,链表直接插到当前链表的最后一个位置上
afterNodeAccess(e);
return oldValue;
}
}
// 快速失败机制,比较当前操作次数与记录次数,是否不一样
++modCount;
// 因为完成一个 pulValue 所以增加一次次数,同时再次比较是否来到阈值
if (++size > threshold)
// 来到阈值,2倍扩容容器
resize();
// true 如果是头结点则删除
afterNodeInsertion(evict);
return null;
}
/**
* Initializes or doubles table size. If null, allocates in
* accord with initial capacity target held in field threshold.
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
* 具有初始化容器大小、2倍扩容容器等功能
* @return the table
*/
final Node<K,V>[] resize() {
// 首次 table 是null
Node<K,V>[] oldTab = table;
// 判断 oldCap 是否为 null 为null 则为0,反之则是容器长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 阈值 首次是0
int oldThr = threshold;
// 新容器容量0 新容器阈值0
int newCap, newThr = 0;
// 容量大于0
if (oldCap > 0) {
// 大于等于最大能容纳的长度
if (oldCap >= MAXIMUM_CAPACITY) {
// 阈值等于最大能容纳的长度
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 容器长度是否大于默认16,且新容器是旧容器的两倍大小且不大于最大容纳长度,
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 新阈值也是旧阈值的两倍大小
newThr = oldThr << 1; // double threshold
}
// 旧阈值 大于 0
else if (oldThr > 0) // initial capacity was placed in threshold
// 新容器长度 = 旧阈值
newCap = oldThr;
else { // zero initial threshold signifies using defaults
// 首次 pull的时候
// 新容器长度 = 16
newCap = DEFAULT_INITIAL_CAPACITY;
// 新阈值等于 负载因子 0.75 * 默认长度 16 = 12
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 这个判断只有在第一次使用构造方法传入一个 Map 才会用到,且这个Map 有值,其他是都基本走不到这里
if (newThr == 0) {
// 容器长度 * 负载因子 0.75
float ft = (float)newCap * loadFactor;
// 只要不大于最大容纳的长度,则 newThr = ft
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;
// 旧容器不为null
// 这是是首次 oldTab 为 null 不走这里,比如第一次 pull 构造方法放入map的子类
// 之后 pull 这里都会走一遍
if (oldTab != null) {
// 链表循环指向下一个节点
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
// 先赋值,当前数组链表的链表给 e 链表且不为null
if ((e = oldTab[j]) != null) {
// 设置数组链表为null
oldTab[j] = null;
// 链表的下一节点为null
if (e.next == null)
// 只有数组链表头有数据,子节点没有数据
// hash 取模,这个模的长度是数组的长度,e 接到新的容器底下
newTab[e.hash & (newCap - 1)] = e;
// 判断 e 是否是树结构
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
// e 不是树结构,且数组链表的子节点也有数据
// 这里应该是计算不同hash的节点,到不同链表上
// 然后迁移到新的容器里,对应的数组链表的位置
else { // preserve order
//
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
// 下一节点
Node<K,V> next;
do {
// e 指向下一节点
next = e.next;
// e 的hash 取模 oldCap,分散。
if ((e.hash & oldCap) == 0) {
// 第一次为null
if (loTail == null)
// loHead 是第一值
loHead = e;
else
// loTail的下一节点 指向 e的下一节点
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;
}
// 2倍扩容之后的数组长度,换句话说,之前的数据,如果 hash 取模之后不等于,之前最后面的位置,则一律变成之前的位置 加上 旧数组长度的。
// 如果之前是 2 数组链表长度是 8,之后这个位置来到 10
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}

总结下,在 pulValue 的时候做了什么。



  1. 先初始化数据结构,数组链表

  2. 如果没有设置初始化大小,则默认长度为16,同时设置阈值为 0.75 * 当前长度

  3. 如果长度来到阈值大小,则进行当前容量二倍扩容,数据大部分会移到新的长度链表里面,少部分会遗留在旧容器长度里面

  4. 查看插入 key 是否存在,有则尾插入,插入之后是否来到 8 ,链表长度来到8则开始转成红黑树。没有,直接插入

  5. 然后 key 的位置里,在放入 value。完成关联

  6. 记录操作次数

  7. 插入后,在继续判断是否扩容



上面,少了链表如何转红黑树。接下来,我们继续看看



/**
* Replaces all linked nodes in bin at index for given hash unless
* table is too small, in which case resizes instead.
*/
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
// 链表长度是否小于 64,换句话说,树长最长只有64
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
// 当链表长度,来到 8 的时候也会判断是否扩容
resize();
// 确定位置
else if ((e = tab[index = (n - 1) & hash]) != null) {
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);
}
}
/**
* Forms tree of the nodes linked from this node.
*/
final void treeify(Node<K,V>[] tab) {
TreeNode<K,V> root = null;
for (TreeNode<K,V> x = this, next; x != null; x = next) {
next = (TreeNode<K,V>)x.next;
x.left = x.right = null;
// 起始 red = false,当 = false 时候就是 黑色,同时根节放入值,只有首次才会走到
if (root == null) {
x.parent = null;
x.red = false;
root = x;
}
else {
K k = x.key;
int h = x.hash;
Class<?> kc = null;
// 遍历链表
for (TreeNode<K,V> p = root;;) {
int dir, ph;
K pk = p.key;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
x.parent = xp;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
// 这里通过左旋 或者右旋,使得 黑的节点变成父节点,自旋完成后,父节点为黑色,而子节点都是红色
root = balanceInsertion(root, x);
break;
}
}
}
}
moveRootToFront(tab, root);
}

转红黑树的过程中,首先会进行一次判断扩容处理,之后先变成一个个的树节点,在变成红黑树。



其中 red = false 时,为黑,反之则是红。通过左右旋,使其父节点变成黑色,子节点为红色,完成一次平衡处理。



图如下:





首次,root节点就是只有一个,所以直接是黑。当当前树高相差为3,会完成一次自旋,使其平衡,如果是不平衡的节点,则会变成红色,平衡的节点则为黑色。



HashMap 的数据结构图解



从上面,我们基本了解了,hashMap是怎么扩容的,如何链表转红黑树、如何定位位置。接下来我们看看HashMap的数据结构是如何。



起始的结构是由数组链表里面且是一个kv结构。头部是一个数组,k的位置是一个 hash 取模(位扰动之后的hash)之后,确定的位置。



后来当,hash冲突变多的时候,数组底下的链表就是存储 hash 冲突之后存储的位置,这个长度大于8的时候转红黑树,包括头,也就是数组,底下的链表长度是7。



优化以及使用 HashMap 的建议



既然读源码,就是为了更好的了解这个东西,只为读而读,为面试而读,这个源码读的没什么意思。学完得要有自己体会吧~



  1. 当使用的 HashMap 是固定不变或者未达到 2 的n次方长度,比如只使用到3,而当前 HashMap 长度为4,可以直接设置负载因子,固定其长度,避免过早的扩容,而浪费空间。

  2. HashMap 默认初始化大小为16,如果添加的值,不到16的话,甚至只有它的一半,这样很容易造成空间上的浪费,之后过多的空间浪费,从而触发 GC 回收,降低整个系统的响应时间。所以尽可能的初始化大小,避免空间的浪费

  3. 当使用的 HashMap 长度超过16,都是没有初始化容器大小,来到阈值大小,频繁的触发扩容机制,导致 HashMap 性能下降。所以也尽可能的初始化大小,避免频繁触发扩容机制。

  4. HashMap 线程不安全,这里主要是这两点(JDK8)通过源码我们知道,在添加元素的时候,在添加前以及添加后都会判断是否扩容,而改变 kv 的分布。在多线程中,如果 A 线程先添加完值,改变了整个容器值的分布,而 B 线程拿到的是原先容器大小来判断位置,从而导致插入的值不在正确的位置上。HashMap 里面有记录操作次数,在多线程中,当前的操作次数,与记录操作次数,可能会不正确,(可能拿到旧值)而出发快速失败机制。

  5. 只要是继承了 Map 接口的子类,都可以转成 HashMap,在创建对象的时候。



声明



作者: Sinsy

本文链接:https://blog.sincehub.cn/2020/10/31/jdk-hashmap/

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文声明。

如您有任何商业合作或者授权方面的协商,请给我留言:550569627@qq.com



引用





发布于: 4 小时前 阅读数: 3
用户头像

苏格兰、情调

关注

还未添加个人签名 2019.10.18 加入

还未添加个人简介

评论

发布
暂无评论
jdk 源码系列之HashMap