HashMap在工作中是最常用的一种数据结构,那么深入了解它的工作原理也成了必不可少的一环,而且大部分面试都会问到HashMap底层原理,学习它益处多多。
HashMap是基于数组+单向链表+红黑树等基本数据结构组成的一种存储key-value键值对的数据结构。基于hash操作查找速度非常快。
HashMap源码重点关注以下内容:
使用的话大家应该都非常熟练,我就直接跳过直奔源码。
提示:本次源码解析不包含链表转换成红黑树过程,红黑树的增删改查操作。如有兴趣可以自己深入学习。
主要属性如下:
/**
* 默认初始化大小,如果你直接new HashMap()那初始大小就是16了
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* 一个HashMap最大的存储容量
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 默认的加载因子,即存储数据达到了当前容量0.75就开始扩容
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 链表变成红黑树最小的长度,即如果链表长度达到8就会变成红黑树数据结构,主要是优化查询效率
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 红黑树标称链表最少元素,红黑树中只有6个元素就会变成链表结构
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* 红黑树的最小值,在链表转换成红黑树的时候会判断,如果HashMap大小小于64则进行扩容操作
*/
static final int MIN_TREEIFY_CAPACITY = 64;
/**
* 哈希表
*/
transient Node<K,V>[] table;
/**
* 缓存entrySet(),用于keySet()和values()
*/
transient Set<Map.Entry<K,V>> entrySet;
/**
* 当前哈希表的容量
*/
transient int size;
/**
* 修改次数,用于哈希表的快速失败机制
*/
transient int modCount;
/**
* 下一次扩容所需要达到的数量
*/
int threshold;
/**
* 加载因子
*/
final float loadFactor;
提示:省略了HashMap(Map map)的构造方法
// 没有任何参数的构造方法,这里仅仅是加载因子设置为默认的0.75
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
// tableSizeFor()方法是将你输入的初始化容量变成大于等于你输入的值的最小2的次幂。
// 例如:你输入的是11,tableSizeFor()方法计算后得到16(大于11且为2的4次方)
// 再如:23->tableSizeFor()->32(2的5次方)
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;
// 这明明是计算后得到的初始化容量,为啥要赋值给下一次扩容所需要达到的数量,put方法中扩容可以找到答案
this.threshold = tableSizeFor(initialCapacity);
}
来看下tableSizeFor()方法,看似简洁,实则深奥
这个方法的目的是返回大于等于参数值且最小的2的次幂,例如
输入11,返回16(2的4次方)。输入16,返回还是16。输入125,返回128(2的7次方)
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;
}
先普及一下位运算:
and & :相同位的两个数字都为1,则为1;若有一个不为1,则为0。
or | :相同位只要一个为1即为1。
>>> :无符号右移, 右移之后, 无论该数是正数还是负数, 右移之后左边都是补上0。
现在我们来看看这个函数:
我们输入参数11
int n = cap -1; 11 -> 10 二进制表示:1010
n |= n >>> 1; 1010右移一位101,然后1010 | 101 = 1111(15)
n |= n >>> 2; 1111右移一位111,然后1111 | 111 = 1111
后面操作一直是1111,即为15,然后返回n+1 = 16
我们输入参数125
int n = cap -1; 125 -> 124 二进制表示:111 1100
n |= n >>> 1; 111 1100 右移一位111 110,然后111 1100 | 111 110 = 111 1110
n |= n >>> 2; 111 1110右移两位111 11,然后111 1110 | 111 11 = 111 1111(127)
后面操作一直是111 1111,即为127,然后返回n+1 = 128
我们再来分析一下:
int n = cap -1; 这个其实是保证输入的是2的次幂返回也是2的次幂。
n |= n >>> 1; 这个保证前2个高位都是1
n |= n >>> 2; 这个保证前4个高位都是1
n |= n >>> 4; 这个保证前8个高位都是1
n |= n >>> 8; 这个保证前16个高位都是1
n |= n >>> 16; 这个保证前32个高位都是1
而int类型最多只有32位,所以保证是2的n次幂减一,最后返回的时候n+1就保证了一定是2的n次幂。
接下来我们看看HashMap最核心的东西–底层数据结构
数组+单向链表,如下图所示:
static class Node<K,V> implements Map.Entry<K,V> {
// node节点存储了哈希值,key,value,和下一个的指针。说明是单向链表
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;
}
}
最重要也是使用的最多的方法就是put和get方法了。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
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) // (n-1)&hash可以说是等价于hash%n,但是前者性能更好
tab[i] = newNode(hash, key, value, null);// 如果通过hash函数之后没有hash冲突,则就放在数组下标
else {
Node<K,V> e; K k;
if (p.hash == hash && // 先比较hashCode然后再equals方法比较。
((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) { // 表示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;
}
get方法比较简单,就是先通过 hash函数找到在数组的哪个下标,然后再通过hashCode和equals找到相等的key,返回value就好了,如果没找到,则返回null。
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return null;
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
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;// 相等直接返回
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;
}
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&// 扩容就是乘以2
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // 走到这里说明是使用的有初始化容量的构造方法,例如HashMap(12)
newCap = oldThr;
else { // 使用HashMap(),默认16,,扩容临界值16*0.75=12
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {// 如果临界值为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) { // 表示小数组要扩容成乘2的大数组,那么位置也要变
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null) // 没有hash冲突则重新hash一下
newTab[e.hash & (newCap - 1)] = e;// 可以尝试一下hash&(oldCap*2-1)和hash&(oldCap-1)有啥区别
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;
if ((e.hash & oldCap) == 0) {// 只看hash值的那一位是0还是1,如果是0那就还是在原来的链表中,这里称之为low链表
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {// 表示那一位是1,则表示high链表,即j+oldCap
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {// low链表在原来位置
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {// high链表在(oldCap+j)位置
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
其他方法有兴趣可以自己去阅读源码,这里就不一一展开介绍。
该方法返回entry的一个set集合,而Node就是实现了Entry,实际上就是node节点的集合。
public Set<Map.Entry<K,V>> entrySet() {
Set<Map.Entry<K,V>> es;
return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}
final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
public final int size() { return size; }
public final void clear() { HashMap.this.clear(); }
public final Iterator<Map.Entry<K,V>> iterator() {
return new EntryIterator();
}
}
从源码中可以看出,这个entrySet是空的,那增强for循环的时候为啥还能遍历出数据呢?增强for循环语法糖其实就是通过迭代器遍历的。而EntrySet重写了iterator()方法,返回的是EntryIterator对象,那么遍历操作就是在这里面做的。
final class EntryIterator extends HashIterator
implements Iterator<Map.Entry<K,V>> {
public final Map.Entry<K,V> next() { return nextNode(); }
}
abstract class HashIterator {
Node<K,V> next; // next entry to return
Node<K,V> current; // current entry
int expectedModCount; // for fast-fail
int index; // current slot
HashIterator() {
expectedModCount = modCount;// 后面如果这两个不相等则表示有线程改了数据,就会抛ConcurrentModificationException,所谓的"fail-fast"
Node<K,V>[] t = table;
current = next = null;
index = 0;
if (t != null && size > 0) { // advance to first entry
do {} while (index < t.length && (next = t[index++]) == null);//找到第一个不为null的数组下标,赋值给next
}
}
public final boolean hasNext() {
return next != null;
}
// next()就是调用的这个方法
final Node<K,V> nextNode() {
Node<K,V>[] t;
Node<K,V> e = next;
if (modCount != expectedModCount)// 快速失败
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
if ((next = (current = e).next) == null && (t = table) != null) {// 表示当前数组下标没有hash冲突
do {} while (index < t.length && (next = t[index++]) == null);// 那么就找下一个有值的数组下标
}
return e;
}
public final void remove() {
Node<K,V> p = current;
if (p == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
current = null;
K key = p.key;
removeNode(hash(key), key, null, false, false);
expectedModCount = modCount;
}
}
EntryIterator 继承了HashIterator,最终遍历还是通过HashIterator。
这个方法返回key的set集合。源码跟entrySet()差不多,还是通过HashIterator遍历,只是next()方法不一样,它获取的是key,这里就不展开介绍了。
public Set<K> keySet() {
Set<K> ks = keySet;
if (ks == null) {
ks = new KeySet();
keySet = ks;
}
return ks;
}
final class KeySet extends AbstractSet<K> {
public final Iterator<K> iterator() { return new KeyIterator(); }
}
final class KeyIterator extends HashIterator
implements Iterator<K> {
public final K next() { return nextNode().key; } // 关键就是这里
}
这个方法返回value的Collection集合。前两个都是set集合,为啥不一样?因为value在不同的key可能有重复的。而entry和key肯定是不会重复的。
这个源码也跟entrySet()差不多,next()方法是返回的value。
public Collection<V> values() {
Collection<V> vs = values;
if (vs == null) {
vs = new Values();
values = vs;
}
return vs;
}
final class Values extends AbstractCollection<V> {
public final Iterator<V> iterator() { return new ValueIterator(); }
}
final class ValueIterator extends HashIterator
implements Iterator<V> {
public final V next() { return nextNode().value; }// 这里不一样
}