HashMap源码
1、HashMap
HashMap是基于哈希表的 Map 接口的实现。允许使用 null 值和 null 键。此类不保证数据顺序(Hash取模长度),特别是它不保证该顺序恒久不变(扩容)。且是非线程安全的。
2、结构
相比List,Hash的类图结构相对没那么复杂
2.1、Map
先来看下顶级接口Map
文档说明:
1、Map是一个将键映射到值的对象。映射不能包含重复的键;每个键最多可以映射到一个值。
2、这个接口取代了Dictionary类,后者是一个完全抽象的类,而不是一个接口。
3、提供了三个集合视图,允许将Map的内容视为一组键、一组值或一组键-值映射。映射的顺序被定义为映射集合视图上的迭代器返回元素的顺序。
Map接口抽象了一些通用操作。定义了一个大致的操作骨架。
Set<Map.Entry<K, V>> entrySet();
个人认为是Map的核心方法,因为后序的大部分操作都依赖此方法。
返回了一个Entry的Set集合,
2.1.1、Entry
Entry是Map的数据存储结构,存储Key和Value的映射关系,抽象了操作Entry的通用方法。其中有四个comparing方法,用于排序。
2.2、AbstractMap
提供了Map接口的骨架实现,以尽量减少实现此接口所需的工作量。
public abstract Set<Entry<K,V>> entrySet();
entrySet方法被定义为抽象的,意味着需要子类去实现它。
2.2.1、SimpleEntry
实现了Entry接口,保存Key和Value的数据结构,包含了Key和Value的成员属性.Key是final类型的。
2.2.2、SimpleImmutableEntry
实现了Entry接口,保存Key和Value的数据结构,包含了Key和Value的成员属性.Key和Value是final类型的。
所以这个类定义了一个不可变的键值映射关系。
2.3、HashMap
为了便于操作和存储数据,HashMap定义了很多内部类型。
2.3.1、内部类型
2.3.1.1、Node
Node是HashMap存储数据的数据结构,实现了Entry接口
Node含有四个成员属性
- hash
key的hash散列值 - key
存储键值映射关系的键 - value
存储键值映射关系的值 - next
处理hash冲突时,节点指向的下一条数据
2.3.1.2、KeySet、Values、EntrySet
继承了AbstractSet,实现了一个简单的Set集合功能。返回键值对中键的集合,实际操作的还是HashMap中的Node节点数据。由于同一个key不可能存储多份,所以使用Set。
继承了AbstractCollection,实现了一个简单的集合功能。返回了键值对中值的集合,由于Value是允许出现重复的,所以这里没有继承AbstractSet,而是AbstractCollection。
继承了AbstractSet,返回了一个键值对集合。返回了键值对映射关系的集合,
2.3.1.3、TreeNode
链表转红黑树的节点数据结构。
2.3.2、成员属性
// 默认初始容量,“必须是2的幂”
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认加载因子,这个值是经过大量测试和实践得出的一个权衡时间和空间的值
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// hash冲突时,链表转化为红黑树的阈值
static final int TREEIFY_THRESHOLD = 8;
// 红黑树退化为链表的阈值
static final int UNTREEIFY_THRESHOLD = 6;
// 链表转化红黑树时容器的最小容量
static final int MIN_TREEIFY_CAPACITY = 64;
// 存储数据的容器
transient Node<K,V>[] table;
// 节点数据缓存
transient Set<Map.Entry<K,V>> entrySet;
// 节点数据大小
transient int size;
// 结构变动修改数
transient int modCount;
// table发生扩容的size大小
int threshold;
// 加载因素
final float loadFactor;
2.3.3、构造方法
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) {
// initialCapacity的合法性校验
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// loadFactor的合法性校验
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
// 设置加载因子
this.loadFactor = loadFactor;
// tableSizeFor会返回一个大于initialCapacity且是最小的2的幂
this.threshold = tableSizeFor(initialCapacity);
}
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;
}
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
// 根据加载因子,计算可以容纳s个元素的最小table.length
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
if (t > threshold)
// 取得最接近且大于t的2的幂
threshold = tableSizeFor(t);
}
// 扩容判断,由于Map的加载因子是可通过初始化参数赋值的,要判断参数map的size是否已经大于扩容阈值
else if (s > threshold)
resize();
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
// 将数据插入到Map中
putVal(hash(key), key, value, false, evict);
}
}
}
四个构造器都不会处理table的初始化,table的初始化被延迟到首次插入数据进行。
构造方法 | 描述 |
---|---|
HashMap()无参构造器 | 使用默认参数的HashMap |
HashMap(int) | 根据参数初始化容量,调用HashMap(int, float)构造器初始化。 |
HashMap(int, float) | 根据参数初始化容量和加载因子,这里实际初始化的是threshold,再首次插入数据时会根据threshold的大小进行table的初始化。 |
HashMap(Map<? extends K, ? extends V>) | 初始化HashMap,并将参数Map中的数据插入到Map中。 |
2.3.4、常用方法
2.3.3.1、hash
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
获取key的hash值,这个值与右面计算获取table[]下标值有关。
在java中,int占4个byte,32个bit,而“h >>> 16”,取h的高16位,然后在与h进行异或操作。为什么这么处理,与计算table[]下标值有关。
2.3.3.2、V put(K key, V value)
将键值对关联,插入到HashMap中,如果键 已存在,则替换值,并返回旧值
// 对外的数据插入接口
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
// HashMap内部插入数据实现
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 首次数据写入,table为空,调用resize()初始化table
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// (n - 1) & hash 计算table下标值,若table下标数据为空,则创建节点插入数据,否则修改数据
if ((p = tab[i = (n - 1) & hash]) == null)
// 创建节点信息,插入table指定下标
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// 判断下标位节点key与参数key是否一致,条件是hash值一致且key相同或key的内容值相等
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 如果节点是树节点,则调用树的putTreeVal写入数据
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 无限循环,统计当前循环节点个数,binCount用于判断是否链表转红黑树
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比较
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;
}
2.3.3.3、Node<K,V>[] resize()
resize会初始化table或对table进行扩容,如果table为空,则根据threshold对table进行初始化,否则按当前table的长度进行2的幂次扩容
扩容时,会将链表节点对半拆分,分别挂在到newTab[j]和newTab[oldCap + j]
final Node<K,V>[] resize() {
// 扩容前的table
Node<K,V>[] oldTab = table;
// 扩容前的table容量
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 扩容前的阈值
int oldThr = threshold;
int newCap, newThr = 0;
// 如果旧容量大于0,则进行2的幂次扩容
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; // double threshold
}
// 旧容量为0,旧阈值大于0,则按旧阈值初始化table,这里会在除了无参构造器的其他其他构造器初始化HashMap时执行
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
// 使用无参构造器初始化HashMap时执行,设置默认容量和阈值
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"})
// 根据newCap对table进行扩容
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
// 判断旧容器是否为空,不为空则需要将旧容器的节点信息插入到扩容后的新容器
if (oldTab != null) {
// 从0开始遍历旧容器
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
// 后继节点不存在,则根据hash值与新容器容量取模得到下标,插入到新容器
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
// 后继节点存在,且是红黑树节点,则对数进行切割,挂到新容器中
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
// 节点有后继节点,且是链表节点,遍历链表,对节点进行再分配,插入到新容器。
// 这里定义了两个头结点和尾结点,代表两个链表,hash & oldCap 为0的,为一个链表,否则为另一个链表,hash & oldCap 为0的链表插入到newTab[j],否则插入到newTab[j + oldCap]
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
// 将链表分割为两个链表的条件,这里我的理解是,忽略余数,如果hash值是oldCap的奇数倍,则一个队列,偶数倍为另一个队列
if ((e.hash & oldCap) == 0) {
// 尾结点为空,则说明头结点为空,设置头结点
if (loTail == null)
loHead = e;
else
// 当前循环节点挂在尾结点后
loTail.next = e;
// 重置尾结点为当前节点,由于初始状态head和tail都为空,插入第一个元素时,head和tail都指向同一个节点,从第二个节点开始,tail的实际意义变为上一个节点信息,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;
}
2.3.3.4、void treeifyBin(Node<K,V>[] tab, int hash)
链表树形化,如果采用链表处理hash冲突,当链表长度越来越长,hashmap的效率会越来越慢,因为链表需要从头开始遍历。时间复杂度为O(n)
链表树形化后,时间复杂度O(logn)
// 链表树形化,树化是对某个节点链表进行树化,通过入参hash值可以计算得出下标,然后对下标出的链表进行树化
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
// 树形化条件,tab.length 必须大于等于 64,否则进行扩容处理
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
// 树的首尾节点,hd为head, tl为tail
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记录的是尾结点,即上一次循环的节点
tl = p;
} while ((e = e.next) != null); // 循环结束条件是 节点不存在后继
// 上面的循环结束后,实际只是从单向链表转成了一个双向链表,如果hd不为空,则还要将双向链表转为数
if ((tab[index] = hd) != null)
// 树化双向链表
hd.treeify(tab);
}
}
// 双向链建树形化
final void treeify(Node<K,V>[] tab) {
TreeNode<K,V> root = null;
// 遍历链表,x是当前节点,next是后继节点
for (TreeNode<K,V> x = this, next; x != null; x = next) {
next = (TreeNode<K,V>)x.next;
x.left = x.right = null;
if (root == null) {
// 设置红黑树根节点即基本属性
x.parent = null;
x.red = false;
root = x;
}
else {
// 记录节点key
K k = x.key;
// 记录节点hash
int h = x.hash;
// 记录key类型
Class<?> kc = null;
// 遍历树
for (TreeNode<K,V> p = root;;) {
// dir 标识节点在左侧还是右侧,ph为树节点hash值
int dir, ph;
// 记录树节点key
K pk = p.key;
// 比较树节点hash和链表节点hash值,树节点hash大于链表节点hash,则再左侧,小于则在右侧,
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
// 处理hash值相等的情况
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
TreeNode<K,V> xp = p;
// 根据dir判断左边还是右边
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;
}
}
}
}
// 将根节点设置到tab中
moveRootToFront(tab, root);
}
2.3.3.3、get(Object key)
// 根据键获取值
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;
// 根据(n - 1) & hash获取下标值
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 判断下标节点key与参数key是否相等
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
// 如果后继节点是树,则获取树节点信息,否则遍历链表,比较key和hash值
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;
}