一、概述
本文将以 涉及到的数据结构->类的属性->构造方法->常用的API 的顺序来阅读源码。
HashMap又称为哈希桶,在JDK1.8前,HashMap的实现方式为:数组+链表,往桶里面添加元素时是通过计算hash值来确定该元素在桶中的位置,即hash值对应着数组的下标,当多个元素的hash值相同时,这些元素会存储在一个链表上,从而解决了冲突。当hash值相同的元素过多时,即一条链表上的节点过多时,通过key查找的效率较低,因此在JDK1.8时HashMap采用 数组+链表+红黑树 实现,当链表长度大于等于阀值8时,链表结构会转换为红黑树结构,从而大大提高了查询效率。
下图为JDK1.8之前的HashMap的数据结构图,左边部分为哈希表,也叫哈希数组,该数组里面的每个元素都是一条单链表的头结点,当多个元素通过key计算得到hash值相同时,就将它们挂到同一条链表上。
上述结构中,在单链表查找一个元素的时间复杂度为o(n),当单链表中的节点数过多时,查找效率会大大降低。在JDK1.8时对此做了优化,当某个链表的长度大于等于8时,该链表结构会转换为红黑树结构,其他长度小于8的链表依然保持链表结构,即JDK1.8后,HashMap为 数组+链表+红黑树混合组成。
JDK1.8后的HashMap数据结构图如下
二、涉及到的数据结构
数组的实现
transient Node<K,V>[] table;
哈希桶数组,显然是一个Node的数组,用来存储单链表的头结点和红黑树的根节点。
链表的实现
Node是HashMap的一个静态内部类,其实就是一个键值对,上图每一个黑点都是一个Node对象。源码实现如下:
//Node是一个单链表实现了Map.Entry<K,V>接口
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;//hash值对应着数组的下标即在数组中的位置
final K key;
V value;//该节点存储的值
Node<K,V> next;//指向的下一个节点,用来关联hash值相同的节点
//构造函数
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; }
//计算每个节点的hash值,通过key的hash值和value的hash值亦或得到
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
//给该节点设置新值,返回旧值
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
//判断两个节点是否相等,通过判断两个节点的key和value值;也可以和自身做比较,返回值为true
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;
}
}
红黑树的实现
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;//颜色属性,true代表该节点为红色,false代表该节点的颜色为黑色
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;
}
}
由于红黑树部分内容较多,其他方法在下文需要时在做详解。
三、类的属性
//序列化Id 很大程度上避免反序列化过程的失败。比如当版本升级后,我们可能删除了某个成员变量,也可能增加了一些新的成员变量,这个时候我们的反序列化依然能够成功,程序依然能够最大程度地恢复数据
private static final long serialVersionUID = 362498820763181265L;
//数组的默认初始大小 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//数组的最大容量 2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认的加载因子,用来计算哈希表元素(数组元素)数量的阀值,threshold=数组长度*加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//链表结构转换为红黑树结构的阀值,当某个单链表中的节点数大于等于该值时会发生转换
static final int TREEIFY_THRESHOLD = 8;
//当红黑树上的节点数小于该值时,红黑树结构会转换为链表结构
static final int UNTREEIFY_THRESHOLD = 6;
//当数组中某条单链表变成红黑树后时,此时数组的最小长度为64,如果不到64,则数组必须进行扩容
static final int MIN_TREEIFY_CAPACITY = 64;
//存放单链表头结点和红黑树根节点的数组
transient Node<K,V>[] table;
//用来保存包含键值对的map对象,方便在不知道key的情况下遍历map对象
transient Set<Map.Entry<K,V>> entrySet;
//数组中存放元素的个数,不是数组的长度
transient int size;
//每次扩容时或者改变结构时加一,即结构被修改的次数
transient int modCount;
//数组扩容的临界值,当数组当前的元素个数大于等于(当前数组的长度*加载因子临界值的值时,数组就会进行扩容
int threshold;
//加载因子
final float loadFactor;
四、构造函数
指定数组的初始化容量和加载因子
public HashMap(int initialCapacity, float loadFactor) {
//数组的初始化容量不能为0
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +initialCapacity);
//如果数组的初始化容量大于规定的最大容量,则将数组的容量定为最大容量
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//加载因子不能为0,不能是负数,不能是非数字
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +loadFactor);
//初始化加载因子
this.loadFactor = loadFactor;
//初始化数组扩容的临界值
this.threshold = tableSizeFor(initialCapacity);
}
tableSizeFor方法
static final int tableSizeFor(int cap) {
//>>>无符号右移
int n = cap - 1;
n |= n >>> 1;//将n和n右移1位的值进行或运算,将结果赋值给n
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
该主要功能是返回一个比给定整数大且最接近的2的幂次方整数,如给定10,返回2的4次方16。
指定数组的初始换容量
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
该构造方法本质还是调用的是HashMap(int initialCapacity, float loadFactor)这个构造方法,只是将loadFactor赋值为默认加载因子。
无参数
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
该无参构造方法只是初始化加载因子为默认加载因子
用已有的Map对象来创建
public HashMap(Map<? extends K, ? extends V> m) {
//初始化加载因子为默认加载因子
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
putMapEntries方法
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
//得到m的元素数量
int s = m.size();
//如果m的元素数量大于0
if (s > 0) {
//判断数组是否table是否初始化
if (table == null) {
//根据m的元素数量和当前的加载因子计算阀值
float ft = ((float)s / loadFactor) + 1.0F;
//修正阀值的边界,不能超出最大容量
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
//如果新阀值大于旧阀值,通过tableSizeFor函数返回一个新的阀值
if (t > threshold)
threshold = tableSizeFor(t);
}
//如果数组table不为空且m的元素数量大于阀值则需要进行扩容
else if (s > threshold)
resize();//扩容
//遍历m将m的元素添加到当前数组里面
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);
}
}
}
resize方法(重要)
当数组元素的数量达到临界值时或者单链表树形化的时候,数组table会进行扩容
final Node<K,V>[] resize() {
//当前的数组
Node<K,V>[] oldTab = table;
//当前数组的容量
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//当前数组的临界值
int oldThr = threshold;
//初始化新的数组容量和临界值为0
int newCap, newThr = 0;
//判断当前的数组容量是否大于0
if (oldCap > 0) {
//如果当前的数组的容量大于最大容量
if (oldCap >= MAXIMUM_CAPACITY) {
//设置临界值为整形表示范围的最大值
threshold = Integer.MAX_VALUE;
//返回当前的数组,不再进行扩容
return oldTab;
}//否则新的容量为旧的长度的两倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)//如果旧的数组的容量大于等于默认初始容量
newThr = oldThr << 1; // 新的临界值为旧的临界值的两倍
}//如果当前数组是空的且有临界值
else if (oldThr > 0)
//则新数组的容量值为旧数组的临界值
newCap = oldThr;
else { //如果当前数组是空的且没有临界值
//新数组的容量为默认值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;
@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;
//如果旧数组当前位置不为空,则将当前位置的元素赋值给e,顺便将旧数组的当前位置置空,便于gc回收
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
//如果当前元素是链表只有一个元素
if (e.next == null)
//直接存入到新数组里面,e.hash & (newCap - 1)相当于取模运算
newTab[e.hash & (newCap - 1)] = e;
//如果当前元素是红黑树,则将当前元素添加到新数组里面的红黑树
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else {
// 进行链表复制
// 通过计算e.hash & oldCap值来判断旧链表
//在新数组的位置,如果计算结果为0,则说明该链表在新数组
//的位置和在旧数组的位置相同,如果计算结果为1,则该链表
//在新数组的位置为: 原始位置+原数组长度
//用来保存位置不变的链表,loHead头结点 loTail尾结点
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;
}
else {
// 计算结果为1,则该链表
//在新数组的位置为: 原始位置+原数组长度
//链表复制
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;
}
五、 常用API
增、改
put方法
public V put(K key, V value) {
//先根据key值获得hash值,然后调用putVal方法添加元素
return putVal(hash(key), key, value, false, true);
}
hash方法
static final int hash(Object key) {
int h;
//首先获得该对象的hashCode值,然后将该将值和该值右移16位后的值进行异或运算后返回
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
JDK1.8优化了高位运算的算法(h >>> 16),使用了0扩展,无论是整数或者负数,移位的时候都在高位插入0。
putVal方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
Node<K,V>[] tab;
Node<K,V> p;
int n, i;
//判断数组是否为空或者长度为0
if ((tab = table) == null || (n = tab.length) == 0)
//对数组进行扩容,获得扩容后的长度
n = (tab = resize()).length;
//i = (n - 1) & hash用来计算待添加的元素应在数组中的下标
//如果计算出来的位置还未存储元素,则新生成一个链表的结点存放在此处
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//如果计算出来的位置已经有元素存放
else {
Node<K,V> e; K k;
//如果该位置的第一个元素的hash值和key值都和待添加的元素的相同,则直接覆盖value
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//将该位置的第一个元素保存起来
e = p;
//如果该位置的第一个元素的hash值和key值都和待添加的元素的不相同且是红黑树节点
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值和待插入的元素的key值相同
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
//跳出循环
break;
//用来遍历链表
p = e;
}
}
//表示在链表中找到和待添加的元素key值hash值都相同的结点
if (e != null) {
//记录原链表中结点的value值
V oldValue = e.value;
//如果onlyIfAbsent为false或者原来的value值为空
if (!onlyIfAbsent || oldValue == null)
//用待添加的元素的vaule值来替换原来的value值
e.value = value;
//回调
afterNodeAccess(e);
//返回旧值
return oldValue;
}
}
//结构改变时计数器加一
++modCount;
//判断当前数组的元素个数是否大于临界值
if (++size > threshold)
//大于的话对数组进行扩容
resize();
//这是一个空实现的函数,用作LinkedHashMap重写使用。
afterNodeInsertion(evict);
return null;
}
删
remove方法
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
根据给定的key来删除键值对;如果key对应的value值存在,删除该键值对并返回value值,如果不存在,返回null。
@Override
public boolean remove(Object key, Object value) {
return removeNode(hash(key), key, value, true, true) != null;
}
根据给的key和value值进行删除
removeNode方法
//如果matchValue值为true时,必须key和value值都相同时才能删除该结点
//如果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;
//如果数组不为空且根据hash算出来的数组下标对应的位置有元素
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
//node用来保存待删除结点
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;
//如果第一个元素不是待删除的结点,就循环遍历找到待删除的结点并用node保存
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);
}
}
//如果找到了待删除的结点且matchValue的值为false或者找到的结点的value和待删除结点的value相同
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;
//链表长度减1
--size;
afterNodeRemoval(node);
//返回旧的已经删除的结点
return node;
}
}
return null;
}
查
带默认值的get方法(JDK1.8新增)
@Override
public V getOrDefault(Object key, V defaultValue) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? defaultValue : e.value;
}
以key为查询条件,如果找到符合的value则返回该value,否则返回默认的value值
get方法
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
以key为查找条件,如果找到对应的value则返回value,否则返回null。
getNode方法
//通过hash值和key值进行查找value
final Node<K,V> getNode(int hash, Object key) {
//找到的结点存储在e
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//查找过程和删除过程差不多
//如果数组不为空且根据hash算出来的数组下标对应的位置有元素
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//判断该位置第一个元素是不是要找的结点
if (first.hash == hash &&
((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;
}
判断该数组是否含有该key的元素
public boolean containsKey(Object key) {
return getNode(hash(key), key) != null;
}
判断该数组是否含有该value的元素
public boolean containsValue(Object value) {
Node<K,V>[] tab; V v;
//遍历该数组的每一个元素
if ((tab = table) != null && size > 0) {
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next) {
//如果找到相同的vaule返回true
if ((v = e.value) == value ||
(value != null && value.equals(v)))
return true;
}
}
}
//没找到的话返回false
return false;
}
六、 总结
本文主要对HashMap组成部分数组和链表的源码进行了详细的解读,至于红黑树部分,因为比较复杂,所以只是简单的解读了一下,后续有时间的话在再对红黑树进行详解。