1. 数据结构
- JDK 1.7 之前 HashMap 是数组+链表
- JDK 1.8 之后 HashMap 是数组+链表+红黑二叉树
2. 源码
2.1 成员变量
//默认容量大小为16. 必须是2的次幂
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//HashMap 的最大容量, 必须是2的次幂,并且如果构造方法初始化大于 1<<30 ,容量就按照 1<<30 这个值
static final int MAXIMUM_CAPACITY = 1 << 30;
//HashMap 默认的负载因子是0.75f,当 HashMap 中元素数量超过 容量*装载因子 时,进行resize()扩容操作
//比如默认的是 16*0.75 = 12 ,也就是说当插入第 13 个数据时, 就扩容到 32
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//当hash有冲突的时候,其链表长度达到8时将链表转换为红黑树 (这是JDK1.8对HashMap的优化,以前一直都是链表)
static final int TREEIFY_THRESHOLD = 8;
//当扩容的时候小于 6 时将解决 hash 冲突的红黑树转变为链表
static final int UNTREEIFY_THRESHOLD = 6;
/**
* 当需要将解决 hash 冲突的链表转变为红黑树时,需要判断下此时数组容量,
* 若是由于数组容量太小(小于MIN_TREEIFY_CAPACITY)
* 导致的 hash 冲突太多,则不进行链表转变为红黑树操作,转为利用resize()函数对hashMap扩容
*/
static final int MIN_TREEIFY_CAPACITY = 64;
//hashmap的存的东西,也就是数组中每个元素都是 Node(节点),如果说,一个hashmap中存放的数没有哈希冲突,那么他就是个数组
transient Node<K,V>[] table;
//由hashMap中Node<K,V>节点构成的set,也就是说记录hashmap中所有的数据
transient Set<Map.Entry<K,V>> entrySet;
//hashmap当前存储的元素的数量 transient关键字表示此成员变量不需要序列化
transient int size;
transient int modCount;
//临界值 当实际大小(容量*填充比 (capacity * load factor))超过临界值时,会进行扩容
int threshold;
// 加载因子,默认0.75
final float loadFactor;
- 加载因子:如果哈希表中的元素放得太满,就必须进行rehashing(再哈希)。再哈希使哈希表元数增倍,并将原有的对象重新导入新的哈希表元中,而原始的哈希表元被删除。load factor(加载因子)决定何时要对哈希表进行再哈希
- 为什么要引入红黑树
当哈希冲突比较多的时候,因为链表的长度很大 , 链表是不利于查询的,有利于删除和插入,所以引进了红黑树,每一个链表分成奇偶两个子链表分别挂在新链表数组的散列位置。- 具体详解:若桶(hashmap的table数组)中链表元素超过8,会自动转化成红黑树;若桶中元素小于等于6时,树结构还原成链表。红黑树的平均查找长度是log(n),长度为8,查找长度为log(8)=3,链表的平均查找长度为n/2,当长度为8时,平均查找长度为8/2=4,这才有转换成树的必要;链表长度如果小于等于6,6/2=3,虽然速度也很快,但是转化为树和生成树的时间并不会太短。中间的差值7为了防止链表和树频繁的转换,试想如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。
- 为什么负载因子默认为0.75f ? 能不能变为0.1、0.9、2、3等等呢?
0.75是平衡了时间和空间等因素; 负载因子越小桶的数量越多,读写的时间复杂度越低(极限情况O(1), 哈希碰撞的可能性越小); 负载因子越大桶的数量越少,读写的时间复杂度越高(极限情况O(n), 哈希碰撞可能性越高)。 0.1,0.9,2,3等都是合法值。
2.2 构造方法
- 构造方法1 : 默认构造方法的加载因子是load factor(0.75), size 是16, 所有的属性都是默认的
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
- 构造方法2 : 初始化容量和加载因子
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
// 如果size<0的时候抛异常
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
// 如果size>MAXIMUM_CAPACITY(1<<30)的时候,强制设为MAXIMUM_CAPACITY(1<<30)的容量
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// 如果加载因子<=0 或者 加载因子不是个数字 就抛异常
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
//方法返回的值是最接近 initialCapacity 的2的幂,
//若指定初始容量为9,则实际 hashMap 容量为16,因为必须是2的次幂
this.threshold = tableSizeFor(initialCapacity);
}
- 构造方法3 : 初始化容量,默认的加载因子
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
- 构造方法4 : 以一个hashmap来创建一个hashmap
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
3. 节点(节点数组)
成员变量中的Node<K,V>[ ]的Node是HashMap的内部类
static class Node<K,V> implements Map.Entry<K,V> {
// 保存的hash值
final int hash;
// 保存 key
final K key;
// 保存 value
V value;
// 指向下一个Node(节点)
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; }
// key 的 hash 值和 value 的 hash 值的 异或运算符(当两个数字不同时为1,其余为0)
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;
}
}
4. 扩容方式
put方法
主要查看put方法的具体实现。其中resize()方法是用来初始化或加倍table大小
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* @param key 的 hash 值
* @param key
* @param value
* @param onlyIfAbsent 如果为 true , 不改变存在的值
* @param evict 如果为 false,则表处于创建模式。
* @return 以前的值,如果没有则为null
*/
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为null时, 调用resize生成数组table, 并把table赋值给tab
if ((tab = table) == null || (n = tab.length) == 0)
// n表示数组的长度
n = (tab = resize()).length;
// 如果 此数组中的 (n - 1) & hash 这个位置 没有值 ,也就是没有Node这个节点
if ((p = tab[i = (n - 1) & hash]) == null)
// 直接创建一个节点 并且赋值给 tab[i]
tab[i] = newNode(hash, key, value, null);
//表示hash有冲突,开始处理冲突/
else {
Node<K,V> e; K k;
// 比较已有的hash值 和 要插入 key 和 hash 值
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//如果hash值相等且key值相等, 则令e指向冲突的头节点
e = p;
// 如果 已有的key 和 要插入的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);
//若链表上节点超过TREEIFY_THRESHOLD - 1,将链表变为红黑树
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;
}
}
// 如果链表中的key 已经存在
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
//覆盖key 相同的 value 并return, 即不会执行++modCount
afterNodeAccess(e);
return oldValue;
}
}
//修改次数自增
++modCount;
// 如果 size 大于 threshold(阈值),就扩容
if (++size > threshold)
//扩容方法
resize();
afterNodeInsertion(evict);
return null;
}
resize方法
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
// 旧数组的size容量
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 旧的 阈值
int oldThr = threshold;
// 新的容量和阈值初始化
int newCap, newThr = 0;
if (oldCap > 0) {
// 如果 容量 大于最大容量 1<<30,阈值就等于 Int 的最大值 即 不再扩容了
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 两个过程
// 1. 设置新的容量 : newCap(新的容量) = oldCap(旧容量) * 2 扩大为原来的2倍
// 2. 设置新的阈值 : 如果 newCap(新的容量) 小于 最大值 并且 旧容量 大于 默认的容量值 , 新的阈值设为 旧的阈值的2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
// 如果旧表的长度的是0,就是说第一次初始化表
// 如果旧的阈值大于0 , 新的容量=旧的阈值
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
// 表示旧表容量和阈值都等于0 ,表示从未初始化过
else { // zero initial threshold signifies using defaults
// 赋值个默认值
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 如果新的阈值等于0 ,按阈值计算公式(size*加载因子)进行计算
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
// 给全局的threshold阈值赋值
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
// 创建新的 数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
// 赋值给全局的 table
table = newTab;
// 把旧的数组中的值, 按某种规则 迁移 到新的数组中
if (oldTab != null) {
//遍历桶数组,并将键值对映射到新的数组中
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
//置为null, 方便进行GC
oldTab[j] = null;
if (e.next == null)
//说明这个node中没有链表,直接放在新表的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 { // preserve order
// 证明此节点是 链表 的形式解决冲突的
//新表是旧表的两倍容量,实例上就把单链表拆分为两队,
//e.hash&oldCap为偶数是低队,e.hash&oldCap为奇数的是高队
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) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
//低队不为null,放在新表原位置
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
//高队不为null,放在新表 j + oldCap 的位置
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
过程:
- 判断数组是否为空, 如果空就创建新的 ,如果不为空走下面
- 根据传入的key判断 hash值和 key, 来查此数组中该位置是否有值(是否冲突)
- 没冲突直接放到该位置, 有冲突然后判断是红黑树还是链表 ,然后对应put方法
- put之后记录修改的次数, 然后再判断size是否大于阈值 (是否扩容)
get方法
public V get(Object key) {
Node<K,V> e;
// 直接从node里面拿到值
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
/**
* Implements get方法具体实现
*
* @param hash hash for key
* @param key the key
* @return the node, or null if none
*/
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) {
// 比较已有的hash值和要插入key和hash值,就返回此Node
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//否则(即已有的key和要插入的key相等)
//指向下一节点并赋值给e
if ((e = first.next) != null) {
//如果此Node属于红黑树,按照红黑树的方法查询并返回
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
//若不属于红黑树,遍历下一节点比较已有hash值和要插入key和hash值,直到满足条件并返回
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
总结:
jdk1.7采用数组+单链表的方式来储存键值对,
jdk1.8之后采用的是数组+单链表+红黑树,hashmap 默认的size是16,之后每次扩充,容量变为原来的2倍. 最大容量是2的30次幂,默认的加载因子是 0.75,数组(Node节点)是HashMap的主体,链表则是主要为了解决哈希冲突而存在的。
为什么要引入红黑树:当哈希冲突过多时,也就是特定索引位置说链表过长时,由于链表查询难,插入删除比较快,所以用红黑树来优化。也就是说当链表长度大于7的时候,就会转化成红黑树。
hashmap是线程不安全的,因为put不是同步的,然后调用真正的addNode也是不同步的,HashMap存在扩容的情况,会同时拿到一个数组,然后进行扩容,所以扩容的也不一样
转载自 https://juejin.im/post/5b6ba798f265da0f8e1a146a
其中添加了一些自己的见解,如有误请留言指正。