HashMap源码学习
HashMap源码学习(jdk1.8)
HashMap的基本使用
// 创建一个hashmap
HashMap<Integer, String> map = new HashMap<>();
// 往hashmap中添加五个元素
map.put(1,"小明");
map.put(2,"小红");
map.put(3,"小刚");
map.put(2,"小张");
map.put(4,"小天");
// 移除key为2的元素
map.remove(2);
// 获取key为1的元素
System.out.println(map.get(1));
// 输出map
System.out.println(map.toString());
// 输出map的长度
System.out.println(map.size());
得到结果:
可以看到,添加的2号小红不见了,换成了小张,而且我们添加了五个元素,但是map中只存了四个元素,这是为什么呢?
源码分析:
HashMap
继承自AbstractMap
,而AbstractMap
实现了Map接口,但是,HashMap
又实现了Map接口。
据java集合框架的创始人Josh Bloch描述,这样的写法是一个失误;后来不认为这个小小的失误值得去修改,所以就这样存在下来了
public class HashMap<K,V>
extends AbstractMap<K,V>
// Clonealbe表示可以进行克隆,Serializable表示可以进行序列化
implements Map<K,V>, Cloneable, Serializable {
}
重要属性:
// 序列化版本号,有兴趣的可以去看我写的关于序列化、反序列化的博客,在这里就不过多介绍了
private static final long serialVersionUID = 362498820763181265L;
// 中文的意思是:默认初始容量;见名知意,就是hashmap的默认初始化容量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 最大容量,1左移30位相当于乘以2的30次幂,一般不会超出这个范围
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 桶的树化阈值,即当链表长度超过8时(同时要满足最小树形化容量阈值),会转成红黑树结构
static final int TREEIFY_THRESHOLD = 8;
// 桶的链表还原阈值,即当红黑树长度小于6时会转回链表
static final int UNTREEIFY_THRESHOLD = 6;
// 最小树形化容量阈值:即 当哈希表中的容量 > 该值时,才允许树形化链表,默认64,这个值不能小于4*TREEIFY_THRESHOLD
static final int MIN_TREEIFY_CAPACITY = 64;
// transient代表这个属性无法被序列化
// 代表hashmap中的数组,hashmap由数组+链表(红黑树)的形式组成
transient Node<K,V>[] table;
// map的每个键值对都当作一个对象存入了set集合中。那么我们就可以用set集合来迭代这个map的所有值
transient Set<Map.Entry<K,V>> entrySet;
// hashmap中的元素个数 The number of key-value mappings contained in this map.
transient int size;
// 阈值
int threshold;
// 加载因子
final float loadFactor;
重要方法
1、构造方法
// 无参构造,只传入了一个默认加载因子;一般建议通过这种方式创建map
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) {
if (initialCapacity < 0)
// 传入小于0的初始容量,会抛出非法参数异常
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
// 当传入的初始化容量超过给的最大值时,将最大值赋值给初始容量
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
// 传入的负载因子不合规,isNaN的意思是 is not a number,代表传入的不是一个数字
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
// 传入另一个Map的构造函数
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
2、put()方法
// 初始化完成后,需要往里面添加元素
// 注意:调用构造方法时,hashmap中的数组并没有被创建,(除了传入map的构造函数,不过其也是通过调用putVal()方法,putVal()中又调用了resize()方法进行初始化)
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
// hash方法
static final int hash(Object key) {
int h;
/* 当key等于null时(由此可见,hashmap是允许key为null的,不会抛出异常),返回0
当key不等于null时,通过Object中的hashCode()方法得到key的哈希码,然后与他本身右移16位进行异或运算
a.保证高16位也参与计算,我们知道int占4字节 32位,16是中位数
b. 因为大部分情况下,都是低16位参与运算,高16位可以减少hash冲突
总而言之,就是为了通过这个运算来减少hash冲突
*/
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// putVal方法
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,或者长度为0时,说明该数组还未被创建(说明hashmap是在put第一个元素时才创建数组),
所以调用resize()方法进行初始化。
*/
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
/* 通过 tab[i = (n - 1) & hash将hash()方法所计算出的hash值与数组长度减一进行与运算,计算出元素应该存储的位置下标,这样做的目的也是为了减少hash冲突;如果计算出的p位置没有任何元素,就通过newNode方法新建一个node元素并将hash,key,value等元素都存入其中,null位置是next属性,链表中指向的下一个元素,这里还没有产生链表所以是null
*/
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
/* 在p不为null的情况下,即p位置已经存在元素的时候,通过与新元素的hash值比较,以及比较key是否是同一个对象,如果是,则直接用新对象替代原对象,如果不是,继续通过equals方法进行比较,如果equals相等,同样会替换。
*/
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 在新的key值与原key值不同的情况下,判断是否是树节点,如果是,直接调用putTreeVal()方法存入
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 否则为链表节点
else {
// 使用尾插法(1.8)在链表尾部插入
for (int binCount = 0; ; ++binCount) {
// 如果p指向的链表下一个元素为null,即到达了链表的最后一个元素
if ((e = p.next) == null) {
// 在链表末尾插入新节点
p.next = newNode(hash, key, value, null);
// 如果链表长度大于等于桶的树化阈值-1的时候(还需要判断hashmap数组长度是否满足树化条件),执行treeifyBin方法转成红黑树
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;
}
}
// 当e不为null。即发生了hash冲突时
if (e != null) { // existing mapping for key
// 用e中的value替代原来的value,并将原来的value以返回值的形式返回
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
// 访问后回调
afterNodeAccess(e);
return oldValue;
}
}
// 修改次数加一,在进行迭代时,通过比较迭代前后的modCount值,来防止在迭代过程修改map
++modCount;
// 如果map长度达到了阈值,则需要进行扩容
if (++size > threshold)
resize();
// 插入后回调
afterNodeInsertion(evict);
return null;
}
3、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;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 新数组长度为旧数组长度左移一位,即乘2;如果旧数组长度大于等于默认的初始化容量,那么新的阈值也需要乘2
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
// 初始容量置于阈值
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
// 零初始阈值表示使用默认值
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 如果新阈值等于0,则将新的数组长度乘以加载因子得到新的阈值
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
// @SuppressWarnings注解是jse提供的注解。作用是屏蔽一些无关紧要的警告。使开发者能看到一些他们真正关心的警告。 从而提高开发者的效率
@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;
// 将当前j位置的node赋值给e
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
// e.next为null说明这个数组下标只有当前这一个node,直接迁移到新数组中指定位置即可
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
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;
// 将当前node的hash值与原数组长度进行与运算,如果为0,则加到lo链表中
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
// 如果不为0,则将当前node加到hi链表中
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null); // 退出循环条件,即当前node的next为null,为该链表最后一个元素
if (loTail != null) {
// 将原数组的node链表转移到新数组中,下标位置不变
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
// 将原数组的node链表转移到新数组中,下标位置变为原下标+原数组长度
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
4、get()方法
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
// getNode()方法
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等于传入的hash值,并且key是同一对象或者equals()方法返回true,直接返回第一个元素
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);
}
}
// 如果查询不到符合条件的值,则返回null
return null;
}
5、remove()方法
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;
// 当前数组不为空且长度大于0,并且通过计算得到的下标位置的桶不为空
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
// 找到目标元素,将p赋值给node
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
// 在红黑树中查找
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
// 在链表中查找
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
// 如果node不为空,即成功找到了该节点;matchValue默认传入false,!即为true
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;
// 移除后回调
afterNodeRemoval(node);
return node;
}
}
return null;
}
hashmap()
中还有一些其他的方法就不一一介绍了,有兴趣的可以自己去读一读源码,这些方法其实大同小异,能看懂一个方法后,再去看其他的方法就比较轻松了!
常见面试题:
1、为什么数组长度要求必须是2的整数倍?
The default initial capacity - MUST be a power of two.
-
能利用 & 操作代替 % 操作,可以提升性能
计算桶位置时其实是通过key的hash%l数组的length来得到下标,这样做的目的是为了减少哈希冲突;然而,相比于&运算,%运算的效率会低很多,而且,当数组的长度是2的整数倍是index = (n - 1) & hash就等价于hash%n;
-
hashmap扩容的元素迁移过程中,由于数组大小是2次幂的巧妙设定,使得只要检查 “ 特殊位 ” 就能确定该元素的最终定位。
2、为什么hashmap的默认加载因子是0.75
The load factor used when none specified in constructor.
这其实是出于时间和空间平衡的结果,是在经过无数次试验测试后得出来的
当加载因子设置比较大的时候,扩容发生的频率比较低,此时发生 Hash冲突的几率就会提升,因此需要更复杂的数据结构来存储元素,这样对元素的操作时间就会增加,运行效率也会因此降低;而当加载因子值比较小的时候,扩容的门槛会比较低,因此会占用更多的空间,空间利用率低。
3、JDK1.7 VS JDK1.8 比较
不同点 | JDK1.7 | JDK1.8 |
---|---|---|
存储结构 | 数组 + 链表 | 数组 + 链表(红黑树) |
hash值计算 | 4次位运算+5次异或运算 | 1次位运算+1次异或运算 |
数据插入方式 | 头插法(多线程下可能会出现死循环) | 尾插法(不会出现死循环,但可能会造成数据丢失) |
扩容后存储位置的计算方式 | 全部按照原来方法进行计算(即hashCode ->> 扰动函数 ->> (h&length-1)) | 按照扩容后的规律计算(即扩容后的位置=原位置 or 原位置 + 旧容量) |
存放数据的规则 | 无冲突时,存放数组;冲突时,存放链表 | 无冲突时,存放数组;冲突 & 链表长度 < 8:存放单链表;冲突 & 链表长度 > 8:树化并存放红黑树 |
初始化方式 | 单独函数:inflateTable() |
直接集成到了扩容函数resize() 中 |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)