一、底层数据结构
JDK8以后底层使用 数组+链表+红黑树的数据结构,当链表长度大于8并且数组长度大于64,链表自动转为红黑树
node与treenode
hashmap中每一个元素都是一个node对象或treenode对象,node是链表节点,treenode是红黑树节点。
node属性有hash值、key、value、next,treenode无非就是多了记录父节点,左右节点,节点颜色,prev(前驱)节点-即比当前节点小的最大节点。在红黑树中,每个节点都有一个指向前驱节点的指针,用于快速查找前驱节点。
二、底层原理及源码分析
2.1 继承关系
继承了AbstractMap抽象类,实现了Map接口
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable{}
2.2 成员变量
transient Node<K,V>[] table
: 用于存储HashMap中的键值对的数组,每个元素是一个链表节点,该节点存储了一个键值对。transient Set<Map.Entry<K,V>> entrySet
: 用于存储HashMap中的键值对的一个set集合,其中每个元素都是一个Map.Entry类型的对象,该对象包含了一个键值对。transient int size
: 用于记录HashMap中键值对的数量。int threshold
: 表示HashMap中键值对的数量达到该值(容量*加载因子)时,会触发扩容操作。final float loadFactor
: 表示HashMap的加载因子,用于计算threshold。transient int modCount
: 用于记录HashMap中结构发生变化的次数,用于快速失败机制。static final int MAXIMUM_CAPACITY = 1<<30
: 表示HashMap中数组的最大容量,即2的30次方。static final float DEFAULT_LOAD_FACTOR
: 表示HashMap的默认加载因子,为0.75。static final int DEFAULT_INITIAL_CAPACITY = 1<<4
: 表示HashMap的默认容量,为16。static final int TREEIFY_THRESHOLD
= 8: 链表长度,当同时满足10和11时,链表转红黑树。static final int MIN_TREEIFY_CAPACITY
= 64: Hashmap容量(数组长度),当同时满足10和时,链表转红黑树。static final int UNTREEIFY_THRESHOLD
= 6: 当红黑树节点数量小于该值时,树会转化为链表。transient Set<K> keySet
: 用于存储HashMap中的键,它是一个Set集合。transient Collection<V> values
: 用于存储HashMap中的值,它是一个Collection集合。- private static final long serialVersionUID = 362498820763181265L :保证不同版本的HashMap在序列化和反序列化时的版本一致性。
2.3 构造方法
4种构造方法演示
public class Test { public static void main(String[] args) { HashMap<String, String> hm1 = new HashMap(); hm1.put("张翠山", "殷素素"); //指定默认容量 HashMap<String, String> hm2 = new HashMap<>(16); //指定默认容量和加载因子 HashMap<String, String> hm3 = new HashMap<>(16, 0.7f); HashMap<String, String> hm4 = new HashMap<>(hm1); System.out.println(hm4); System.out.println(hm1); } }
注意:
空参构造只是把默认加载因子的值赋给了加载因子这个变量,并没有创建table[]数组,此时的table数组是默认初始值,为null。
2.4 重要的成员方法
2.4.1 put()方法
调用putVal方法,方法参数讲解如下
返回值:返回被覆盖元素的值(value),如果没有覆盖,返回null
putval方法 原码讲解 重点来啦
首先明确一个问题:
hash是根据key值计算出来的,但是key值不同计算出来的hash值也可能相同,这叫hash碰撞,但是hash值不同key一定不同
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { //tab用于保存Hashmap中数组的地址 //tab是在栈上开辟的,访存速度快,而外面的table是在堆上开辟的,访问慢 Node<K,V>[] tab; //要添加的节点p Node<K,V> p; int n, i; //数组没有创建或者数组长度为0进if if ((tab = table) == null || (n = tab.length) == 0){ //如果是第一次添加元素,调用resize()方法,底层会创建一个默认长度为16,加载因子为0.75的数组 //将tab的长度赋给变量n n = (tab = resize()).length; } //不是第一次添加元素 //1.计算要添加元素在数组中的索引,也就是i //2.判断:数组中索引处是否为null //如果为null 直接添加一个新的节点到tab中 //如果不为null,走else,此时p是数组中索引处的节点,默念三遍!!! if ((p = tab[i = (n - 1) & hash]) == null) //是null添加新节点 tab[i] = newNode(hash, key, value, null); else { //不是null Node<K,V> e; K k; //1.判断要添加的hash值与数组中索引处的节点(p)的hash值是否相同 //相同并且key也相同 覆盖元素 //不相同走elseif 或 else if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) //覆盖原数组中的元素 e = p; else if (p instanceof TreeNode) //与p的hash不同 可添加该节点 //是红黑树结构就添加红黑树的节点 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { //与p的hash不同,以p为迭代对象循环找该链表的尾节点 for (int binCount = 0; ; ++binCount) { //判断p是否是尾节点 if ((e = p.next) == null) { //是尾节点,在p的后面添加新的链表节点 p.next = newNode(hash, key, value, null); //添加完后判断是否转为红黑树结构 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } //迭代的过程中有与添加元素重复的key,break跳出,注意此时e是与添加元素重复key的节点 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; //迭代p p = e; } } //如果e是空 添加新元素 //如果e不是空 覆盖元素 if (e != null) { // existing mapping for key //记录老的value V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) //覆盖 e.value = value; afterNodeAccess(e); //返回老的value return oldValue; } } ++modCount; //不用管 //向数组中添加元素需要判断是否达到了扩容时机 达到就扩容 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
再画个图便于你理解
三、高频面试题
问:
Hashmap的工作原理?
Hashmap中发生冲突怎么办?
Hashmap是如何扩容的
如果两个键的Hash值相同,你如何获取这两个Map.Entry对象
当发生冲突并且两节点的key相同时,是如何覆盖元素的
答:
1. HashMap是一种基于哈希表实现的Map接口的键值对存储结构。工作原理可以简单概括为以下几个步骤:根据key值计算hashcode值,将键值对存储到表中,查找键值对。
2. hash值相同叫做发生冲突,发生冲突之后依次比较待添加节点与链表或红黑树中的每一个节点的key值,如果key值相同,覆盖,返回要覆盖的元素的value。如果不相同并且到了尾节点,添加新节点。
3. 当数组长度到达总长度的加载因子倍,扩容为原容量的两倍
4. 当我们调用get()方法,HashMap会使用键对象的hashcode找到bucket位置,然后获取值对象。找到bucket位置之后,会调用keys.equals()方法去找到链表中正确的节点,最终找到要找的值对象。
5.如果这个节点是数组中的,将新的节点替换原节点,如果是链表或红黑树中的节点,只修改原节点的value值。
📕总结
以上是hashmap的底层分析,希望对你有帮助