1 . 数据结构:
- JDK 1.7 之前 HashMap 是数组+链表 俗称哈希表组成 key的 (h = key.hashCode()) ^ (h >>> 16)
- 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 int size;
//
transient int modCount;
//临界值 当实际大小(容量*填充比 (capacity * load factor))超过临界值时,会进行扩容
int threshold;
// 加载因子, 一般没动过这个,用默认的就可以了 0.75
final float loadFactor;
复制代码
- 加载因子:如果哈希表中的元素放得太满,就必须进行rehashing(再哈希)。再哈希使哈希表元数增倍,并将原有的对象重新导入新的哈希表元中,而原始的哈希表元被删除。load factor(加载因子)决定何时要对哈希表进行再哈希
- 为什么要引入红黑树:
当哈希冲突比较多的时候,因为链表的长度很大 , 链表是不利于查询的,有利于删除和插入,所以引进了红黑树,每一个链表分成奇偶两个子链表分别挂在新链表数组的散列位置. - 为什么负载因子默认为0.75f ? 能不能变为0.1、0.9、2、3等等呢?
0.75是平衡了时间和空间等因素; 负载因子越小桶的数量越多,读写的时间复杂度越低(极限情况O(1), 哈希碰撞的可能性越小); 负载因子越大桶的数量越少,读写的时间复杂度越高(极限情况O(n), 哈希碰撞可能性越高)。 0.1,0.9,2,3等都是合法值。
2.2 构造方法
- 构造方法一 : 默认构造方法的加载因子是load factor(0.75), size 是16, 所有的属性都是默认的
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
复制代码
- 构造方法二 : 初始化 容量 和 加载因子
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);
}
复制代码
- 构造方法三 : 初始化容量,默认的加载因子
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
复制代码
- 构造方法四 : 以一个hashmap 来创建一个hashmap
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
复制代码
3. HashMap 的节点
是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() 方法
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;
}
复制代码
过程总结:
- 判断 数组 是否为空, 如果空 就创建个新的 ,如果不为空 走下面
- 根据传入的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;
}
复制代码
resize() 扩容方法机制 和 方法
- 扩容机制:原来的hash值新增的是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”
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;
}
复制代码
总结:
在jdk1.7之前采用 数组+单链表 的方式来储存键值对,在jdk1.8之后采用的是数组+单链表+红黑树,hashmap 默认的size 是16,之后每次扩充,容量变为原来的2倍. 最大容量是2的30次幂,默认的加载因子是 0.75,数组(Node节点)是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,为什么要引入红黑树:当哈希冲突过多时,也就是特定索引位置说链表过长时,咱们都知道链表查询难,插入删除比较快,所以用红黑树来优化,也就是说当链表长度大于7的时候,就会转化成红黑树.hashmap 是 不安全的,因为put不是同步的,然后调用真正的addNode也是不同步的,HashMap存在扩容的情况,会同时拿到一个数组,然后进行扩容,所以扩起来的也不一样.
下图为16扩充为32的resize示意图