一、继承体系
- 继承AbstractMap,实现Map接口,Cloneable,Serializable接口
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
二、初始化方法
//内部关键属性
//正在存储键值对的数组
transient Node<K,V>[] table;
//链表变成树形的,链表长度临界值
static final int TREEIFY_THRESHOLD = 8;
//数组长度大于等于此值时,数组的节点进行树化
static final int MIN_TREEIFY_CAPACITY = 64;
//阈值算法为capacity*loadfactory,大致当map中entry数量大于此阈值时进行扩容(1.8)
//该值是用来判断是否需要进行扩容的临界值,但是在初始化的时候,由于对象里面没有保存设置的数组长度的值
//一开始,调用了容器大小的初始化方法的话,数组并没有初始化,该值记录的是需要初始化的数组大小,而真正的阈值是在第一次创建的时候,使用该值重新创建
//后面, 该值就是用来记录扩容的临界值
int threshold;阈值
1. 无参构造器
- 无参构造器,只设置了默认的负载因子(DEFAULT_LOAD_FACTOR=0.75)
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
2. 存在初始容量的构造器
- 设置Map的初始容量,实质是大于该容器的2的n次方大小,内部调用了两个参数构造方法
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
3. 两个参数的构造方法
- 通过参数设置,初始的容量大小,设置负载因子,值得注意的是该方法才进行内部数组的创建,相比于无参的构造器而言,设置了初始化大小,此处没有针对容器中的数组进行创建,只是赋值了threshold的大小(threshold表示当HashMap的size大于threshold时会执行resize操作。 )
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;
//获取的是,比初始化容量大的最小的2的n次方大小
this.threshold = tableSizeFor(initialCapacity);
}
- 如果设置的容量大小
- 小于0 ,直接报错
- 大于最大值,则为最大值,最大值为1<<30
- tableSizeFor方法
static final int tableSizeFor(int cap) {
int n = cap - 1;
//将容量变成 2的n次方-1的值
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
//如果容量小于0,使用最小值,如果大于最大值就使用最大值,否则是2的n次方的容量
//为的是,后面计算桶的索引时,使用位运算进行
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
//验证
public static void main(String[] args) {
//相当于capcity = 3
byte n = 2; //0010
n |= n >>> 1; //0010 | 0001 = 0011
n |= n >>> 2; //0011 | 0000 = 0011
n |= n >>> 4; // 0011
n |= n >>> 8; // 0011
n |= n >>> 16;
//最后的容量为3+1 = 4 2的2次方大小
}
1. 最大值为1<<30
- 原因,首先1<<31为负数不满足,容量大小的含义
- hashmap中,通过
到目前为止仍然没有对,数组进行初始化
三、新增方法
- 代码
- put(key,value)
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
- 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; //1. 没有进行初始化数组 if ((tab = table) == null || (n = tab.length) == 0) //1.1 初始化tab数组,调用resize()扩容方法 n = (tab = resize()).length; //2. 数组进行过初始化,而且存在值,只是数组下标位置处没有元素,下标解释见下面的内容 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); //3. 数组中存在元素,且当前数组下标的位置存在元素 else { Node<K,V> e; K k; //3.1 头节点与添加的元素相等,直接覆盖 //如果存在下面的情况,即认为是相等的元素 //如果节点的hash值相等,且如果key是基本类型,需要值相等,如果是引用类型,地址相等 //如果节点的hash值相等,且如果key不为空,key的equals方法判断之后相等,所以HashMap中的key一定要重写equals方法 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //3.2 元素类型为树节点,进行树节点新增 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); //3.3 元素类型为链表节点,进行链表节点新增 else { for (int binCount = 0; ; ++binCount) { //3.3.1 找到最后一个非空节点 if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); //如果大于树化临界值,执行树化方法,但是不一定会树化 //执行完方法后,退出循环 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } //3.3.2 如果节点的元素和要添加的元素相等,退出循环 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; //指针移动,指向下一个节点 p = e; } } //如果e不为空就替换旧的oldValue值 if (e != null) { // existing mapping for key V oldValue = e.value; //onlyIfAbsent if true, don't change existing value //onlyifAbsent表示只有不存在才put //此方法正是,putifAbsent的底层实现,用来put时,是否覆盖原有的值,jdk1.8新增的特性 if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
hash算法
- hash方法也称“扰动函数”。
- key.hashCode()函数调用的是key键值类型自带的哈希函数,返回int型散列值。理论上散列值是一个int型,如果直接拿散列值作为下标访问HashMap主数组的话,考虑到2进制32位带符号的int表值范围从-2147483648到2147483648。
- (h = key.hashCode()) ^ (h >>> 16):当数据量很大时,就会存在hash碰撞。所以使用 hash的高16位异或低16位,就是为了混合原始哈希码的高位和低位,以此来加大低位的随机性。而且混合后的低位掺杂了高位的部分特征,这样高位的信息也被变相保留下来。
static final int hash(Object key) {
int h;
//hashCode()方法,如果没有重写的话是Object的方法
//所以如果key是对象的话,需要重写hashCode()方法
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
树化操作
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
//如果数组的长度小于 最小的树化长度,不进行树化,进行扩容
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
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 = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
数组下标的获取
i=(n - 1) & hash
- 原因:
- n为2的n次方,所以n-1为全部为1的二进制表示,数组长度为8(1000),8-1=7(0111)
- 使用hash值,与0111进行与操作,会得到0000~0111共8个数值,从而对应数组下标,使用位运算可以极大的加快计算效率。
resize扩容操作
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
//-------------------------获取数组容量大小---------------------------
//1 数组中不仅调用了构造方法创建,而且存在元素
if (oldCap > 0) {
//1.1 大于最大的容量,就为最大的容量
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//1.2 否则为原始容量的2被
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
//2 调用了两个参数构造方法,设置了threshold为容量,但是没有进行数组创建,可以看之前的构造方法
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
//3 调用了一个参数的构造方法,只设置了负载因子,所以使用默认的大小16以及负载因子进行设置数组容量大小
else { // zero initial threshold signifies using defaults
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;
//存在元素才进行操作
//1 让临时节点e指向,需要操作的数组中对应的地址
if ((e = oldTab[j]) != null) {
//1 设置原始数组的值为null,有助于GC
oldTab[j] = null;
//2. 只有一个元素,直接迁移到新数组中
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
//3. 如果是树节点,进行树节点的扩容变化
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//4. 链表节点,进行扩容变化
else { // preserve order
//索引不变的链表
Node<K,V> loHead = null, loTail = null;
//索引改变的链表
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
//4.1 将链表拆成不用改变索引和需要改变索引的两部分
do {
next = e.next;
//4.1 获取到元素,与原始的数组的长度进行与操作
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);
//4.2 付给新的数组
//原始大小 4 0100
//key1 hash之后的值为 0001 = 1001 & 0011
//key2 hash之后的值为 0001 = 0001 & 0011
//新大小 8 1000
//key1 1001的下标是9 超出了8 可以看到 1000 & 1001 = 1000 存在比之前容量大的高位,所以需要更改
//key1 1001的下标是9 超出了8 可以看到 1000 & 0001 = 0000 不存在比之前容量大的高位,所以不需要更改
//不需要改变索引的链表,直接使用原始的索引
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
//不需要改变索引的链表,原始索引+原始容量
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
四、删除方法
- 根据key删除 remove(Object key)
- remove(key)
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
- removeNode
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;
//1 获取到对应的数组位置
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;
//------------------------------------找节点----------------------------------------
//1 如果头节点是要删除的节点,赋值给node节点
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
//2 如果头结点不是,而且存在后面的节点
else if ((e = p.next) != null) {
//2.1 如果是树节点,使用树节点的查找方式查找
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
//2.2 链表节点查找
else {
do {
//如果对象的hash值相等,且
//key相等,表示地址相等或者值相等
//或者key的值相等
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
//------------------------------------删节点----------------------------------------
//3. 找到节点进行删除
//matchValue if true only remove if value is equal,只有值也相等了才能移除,上面是通过key的一些计算,获取到的key,value不一定相等,所以,尽量实现值的equals方法
//对节点进行校验,可以看出,
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;
--size;
//模板模式,在node移除之后做的操作
afterNodeRemoval(node);
return node;
}
}
return null;
}
五、线程不安全性
- put的时候导致的多线程数据不一致
- resize(),在put的时候是插入到链表尾部,但是扩容之后的插入是头插入,导致和之前的链表顺序的反的,这样在并发的情况下,导致了环形链表。(jdk1.8以前会出现,jdk1.8之后,改成了尾插入,和之前原有的链表连接一致,不会出现环形链表)
六、注意点
- 为什么提倡数组的大小是2的n次方?
- 因为只有大小是2的n次幂方时,才能使hash值%n(数组大小)==(n-1)&hash的公式成立。
七、问题
1. hashmap的扩容流程
- 扩容:包括增加元素之后的扩容,以及数组的初始化
1. 数组存在元素(容量大于0)
|-> 超过了Integer的最大值,不进行扩容,已经存满
|-> 容量扩大至之前的2倍,阈值扩大为之前的两倍
2. 数组初始化设置了容量,但是没有进行实例化(惰性创建)
|-> 将阈值中保存的出事容量大小设置为新的容量大小
3. 数组初始化没有设置容量
|-> 将阈值、容量设置为默认值
4. 数组初始化只设置了容量的情况(上面的2)
* 因为老阈值中保存的是容量大小,而非真正的阈值,所以需要重新计算,赋值
|-> 设置阈值
------5. 树或者链表的扩容操作-----
1. 如果数组,链表的长度
1. 链表长度大于6,且数组的长度<64时,只进行数组扩容
2. 链表长度大于6,且数组的长度>=64时,链表转换为树结构
3. 链表长度小于8时,树会转换为链表结构
2. hashmap的新增元素
------------1. 创建节点-----------
1. 如果数组为空,创建数组,对应扩容操作
2. 如果数组对应的桶为空,在桶空的位置创建一个节点
3. 如果数组对应的桶不为空
------------2.获取新增节点所处的位置-------
1. 头节点:如果key对应的hash相等,且满足下面二选一,则找到新增节点所对应的节点位置
1. 如果是基本类型,必须相等
2. 如果是引用类型,equals必须相等
2. 非头节点,此处使用e节点记录是否存在相同的key的节点,key的判断和头节点的判断一样
1. 如果是树节点,查询树节点中是否存在,相同key的节点
1. 存在,返回相同key的节点,放到e中
2. 不存在,将新增节点增加到树中,同时,返回e为null
2. 如果是链表节点,查询链表节点中是否存在,相同key的节点
1. 存在,返回相同key的节点,放到e中
2. 不存在,将新增节点增加到链表中,同时,返回e为null
3. 如果e不为null,说明有相同key的节点
1. onlyIfAbsent(只有空才赋值)=false或者原始的值为null,直接覆盖
2. onlyIfAbsent=true且原始的值不为为null,不进行操作
-------------3.进行扩容-----------
4. 如果数组大小大于阈值,则进行扩容操作
- 问题:put两个相同key的值,如何操作???
- 如果key的hash不同或者基本类型==不等,或者引用的equals不同,则新增节点
- 两个key完全相等
- 如果onlyIfAbsent(只在空的时候赋值)=false或者,原始的值为null,那么直接覆盖原值
- 如果onlyIfAbsent=true且原始值不为null,则直接跳过,不进行覆盖
优秀文章:
3. https://www.jianshu.com/p/ee0de4c99f87
4. https://blog.csdn.net/weixin_41156348/article/details/96430481