HashMap源码学习
2021/9/5 20:09:18
本文主要是介绍HashMap源码学习,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
HashMap源码学习
简介
HashMap是数组+链表实现,采用key/value的键值对,每个key对应唯一的value,查询和修改的效率很快,能达到O(1)的平均时间复杂度,是非线程安全的且不能保证元素的存储的顺序。
存储结构
采用数组+链表,出现hash冲突的时采用链表解决hash冲突。hashmap定义了一个数组变量transient Node<K,V>[] table;Node是一个静态内部类实现了Map.Entry<K,V>,采用链表指向下一个节点。
final int hash; final K key; V value; Node<K,V> next;
HashMap采用数组+链表+红黑树,一个数组下标位存储Node链表。在添加元素时会根据key计算出hash值算出在数组的下标位。
当链表长度超多了8时会转化为红黑树并再元素减少到6时候又会把红黑树转为链表来提高效率。数组的查询效率O(1),链表是O(n),红黑树的查询效率O(log n)。
属性变量
/** * 默认数组长度为16,HashMap空构造函数时第一次扩容的时候用 */ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; /** * 最大的容量为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; /** * 当数组的长度大于 */ static final int MIN_TREEIFY_CAPACITY = 64; /** * 数组,又叫作桶(bucket) */ transient Node<K,V>[] table; /** * 作为entrySet()的缓存 */ transient Set<Map.Entry<K,V>> entrySet; /** * 元素的数量 */ transient int size; /** * 修改次数,用于在迭代的时候执行快速失败策略 */ transient int modCount; /** * 当桶的使用数量达到多少时进行扩容,threshold = capacity * loadFactor */ int threshold; /** * 装载因子 */ final float loadFactor;
内部类
HashMap的数组是Node数组,Node是一个典型的单链表节点,其中,hash用来存储key计算得来的hash值。
static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; 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; } }
上面提到当链表会转化为红黑树,HashMap定义了TreeNode的静态内部类,是一个树形结构。定义如下
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { TreeNode<K,V> parent; // red-black tree links TreeNode<K,V> left; TreeNode<K,V> right; TreeNode<K,V> prev; // needed to unlink next upon deletion boolean red; }
构造方法
空构造方法,属性使用默认值
public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; //指定扩容因子0.75 }
指定容器大小和扩容因子的构造函数,HashMap(int initialCapacity)也是调用这个函数
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; this.threshold = tableSizeFor(initialCapacity); }
put(K key, V value)
添加元素
public V put(K key, V value) { //调用hash(key)计算key的hash值 return putVal(hash(key), key, value, false, true); } static final int hash(Object key) { int h; //若key为null,则hash值为0;否则使用key的hashcode获取hash并让hash和高位的16位异或获取hash值 //int类型4个字节32位,相当于让高16位和低16位异或,保证计算出的hash更分散。 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } //往容器添加元素 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { //tab数组临时变量,p用于链表查找,n记录数组长度,i数字索引临时变量。 Node<K,V>[] tab; Node<K,V> p; int n, i; //当数组位空或数组长度为0时,进行扩容范围新的数组长度 if ((tab = table) == null || (n = tab.length) == 0) //调用resize进行扩容 n = (tab = resize()).length; //hash值与数组长度与操作(n-1)&hash计算当前的key的索引位,如果当前索引位为空,直接为该索引位赋值 if ((p = tab[i = (n - 1) & hash]) == null) //新建node结点赋值给指定索引位 tab[i] = newNode(hash, key, value, null); //如果当前索引位有值,则说明key是hash冲突要处理链表逻辑 else { Node<K,V> e; K k; //如果索引位第一值和新增key的hash相同且key值相等时,记录该node结点为e,用于后续数据覆盖 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //如果第一个元素是树节点,则调用树节点的putTreeVal插入元素 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { //遍历该索引位链表,bitcount用于存储链表中元素的个数 for (int binCount = 0; ; ++binCount) { //如果下一个结点为空,则新增一个结点到p节点之后(尾插),并跳出当前循环 if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); //新增结点之后链表长度大于等于8时转化为红黑树 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st //当链表长度大于等于8时 //treeifyBin逻辑:先判断数组大小是否小于64,是就调用resize扩容;否则把链表树化。 treeifyBin(tab, hash); break; } //判断链表中是否有其他相同的key,并记录结点 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } //当容器中存在相同的key值时,e不为null if (e != null) { // existing mapping for key V oldValue = e.value; //判断是否需要替换旧值 if (!onlyIfAbsent || oldValue == null) e.value = value; //在节点被访问后做点什么事,在LinkedHashMap中用到 afterNodeAccess(e); return oldValue; } } //说明没有找到相同的key,修改次数+1 ++modCount; //存储元素数量+1并判断是否需要扩容 if (++size > threshold) resize(); // 元素数量加1,判断是否需要扩容 afterNodeInsertion(evict); return null; } //在添加元素时,链表长度大于等于8时会判断进行树化处理。 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(); //当数组长度小于64时,直接扩容 //数组索引位链表不为空是转化为红黑树 else if ((e = tab[index = (n - 1) & hash]) != null) { TreeNode<K,V> hd = null, tl = null; do { //把Node链表转化为红黑树。 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); } }
总结HashMap添加元素:
- 根据key调用hash(key)方法获取hash值、颞部采用key值的高16位和低16位异或得到
- 调用putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict)方法
- 首先判断数组长度是否为0,是就进行扩容。
- 数组长度不为0,则hash与数组长度与运算确定数组下标位。如果该下表为不存在值直接赋值
- 若数组位存在值则判断是否存在相同的key,存在则记录老key的节点并替换;若不存在则在链表尾部添加节点,添加后判断链表长度并判断是否需要转化为红黑树
- 添加新节点之后,容器元素+1并与扩容值判断是否进行扩容。
resize方法
HashMap允许自动扩容,put()知道HashMap在数组长度为0或容器元素数量size大于扩容阈值事进行扩容。
final Node<K,V>[] resize() { //扩容前的数组,老数组 Node<K,V>[] oldTab = table; //老数组长度大小 int oldCap = (oldTab == null) ? 0 : oldTab.length; //老数组的扩容点,大于这个值才进行扩容 int oldThr = threshold; int newCap, newThr = 0; //如果老数组长度大于0 if (oldCap > 0) { //老数组长度大于等于容器最大值1<<30,则不进行扩容了 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } //如果老数组长度*2还小于等于最大数组长度且老数组长度大于等于16时进行双倍扩容。 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 threshol //使用非默认构造方法创建的map,第一次插入元素会走到这里 //如果旧容量为0且旧扩容门槛大于0,则把新容量赋值为旧门槛 newCap = oldThr; else { // zero initial threshold signifies using defaults //调用默认的无参构造函数,在第一次添加元素时会走这里 //新的数组长度设置为默认值16,扩容阈值为:数组长度*扩容因子。默认是16*0.75=12 newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) { // 如果新扩容门槛为0,则计算为容量*装载因子,但不能超过最大容量 float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } //把新的扩容阈值设置给hashmap的属性 threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) //新建一个新容量的数组 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; //把hashmap的属性table赋值为newTable table = newTab; //如果老数组不为空则开始数组迁移 if (oldTab != null) { //遍历老数组 for (int j = 0; j < oldCap; ++j) { Node<K,V> e; //如果老数组中第一个元素不为空,赋值给e if ((e = oldTab[j]) != null) { oldTab[j] = null; //如果该数组下标位只有一个节点则把节点重新定位下标位并迁移到新数组。 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 //走到这里说明链表不止一个元素且不是红黑树 //因为是两倍扩容的,需要把该链表分拆成两部分插入到新表中。 //比如老数组长度是4,则hash值为3,7,11,15在数组下标3的位置。 //扩容后长度就是8了,则3和11还在3号位,7和16则在7号位了 //用四个节点去接收,head节点直接赋值给数组下标位 Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; //next临时变量,遍历链表使用 Node<K,V> next; do { next = e.next; //(e.hash & oldCap) == 0的元素放在低位链表中如3&4=0 if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else {//(e.hash & oldCap) != 0的元素放在高位链表中,如7&4!=0 if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); //遍历完链表之后赋值。当低链表还存在值时赋值在老数组原本的下标位置 if (loTail != null) { loTail.next = null; newTab[j] = loHead; } //扩容时按照双倍扩容的,高位链表正好赋值到老数组下标位+老数组长度的位置(即3号位迁移到7号位) if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; }
扩容机制总结
1 在容器元素数量大于扩容阈值时进行扩容
2 使用是空构造函数,则第一次插入元素时初始化为默认值,容量为16,扩容门槛为12;
3 使用的是带参的构造方法,则第一次插入元素时初始化容量等于扩容门槛,扩容门槛在构造方法里等于传入容量向上最近的2的n次方;
4 旧数组长度大于0,则新数组等于旧数组的2倍,但不超过最大长度为2的30次方,新扩容阈值为旧扩容阈值的2倍;
5 创建新的数组,数组长度为扩容后的大小
6 把老数组中的数据迁移到新数组,链表分为2部分。hash&老数组长度为0的低位节点保存在原索引位、不为0的高位节点保存在原下标位+老数组长度的下标位。
get(Object key)
根据key值获取value值,时间复杂度O(1)。
public V get(Object key) { Node<K,V> e; //与put添加元素一样,调用hash(key)方法计算key的hash值 return (e = getNode(hash(key), key)) == null ? null : e.value; } final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; //根据hash&数组长度得到下标位,该下标位有值 if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { // 第一个元素是不是要查的元素,如果是直接返回 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); //否则就遍历整个链表直到找到要查找的值,没有则返回null。 do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; } final TreeNode<K, V> getTreeNode(int h, Object k) { // 从树的根节点开始查找 return ((parent != null) ? root() : this).find(h, k, null); } final TreeNode<K, V> find(int h, Object k, Class<?> kc) { TreeNode<K, V> p = this; do { int ph, dir; K pk; //pl左子数。pr右字树 TreeNode<K, V> pl = p.left, pr = p.right, q; //如果p节点的hash值大于h,则从左字树遍历 if ((ph = p.hash) > h) p = pl; //若p节点的hash值小于h,则从右节点遍历 else if (ph < h) p = pr; // 找到了直接返回 else if ((pk = p.key) == k || (k != null && k.equals(pk))) return p; // hash相同但key不同,左子树为空查右子树 else if (pl == null) p = pr; //右子树为空查左子树 else if (pr == null) p = pl; else if ((kc != null || (kc = comparableClassFor(k)) != null) && (dir = compareComparables(kc, k, pk)) != 0) // 通过compare方法比较key值决定使用左子树还是右子树 p = (dir < 0) ? pl : pr; else if ((q = pr.find(h, k, kc)) != null) // 以上条件都不通过,尝试在右子树查找 return q; else // 都没找到就在左子树查找 p = pl; } while (p != null); return null; }
remove(Object key)
public V remove(Object key) { Node<K,V> e; //调用hash方法计算key的hash值 return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value; } 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; // 第一个元素正好就是要找的元素,赋值给node变量用于后续删除使用 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) node = p; //该下标位是长度大于1的链表 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); } } //找到了元素,则看参数是否需要匹配value值,如果不需要匹配直接删除,如果需要匹配则看value值是否与传入的value相等 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 //把node节点删除。p的后续节点指向node的手续节点。 p.next = node.next; ++modCount; --size; // 删除节点后置处理 afterNodeRemoval(node); return node; } } return null; }
hashmap根据key值删除的逻辑
1 计算key的hash值并计算key在数组的下标位
2 查找key容器中的节点应用node。树形节点调用TreeNode类实现,链表使用do while循环查找
3 删除节点,容器size-1。
其他常用方法
//清空数据 public void clear() { Node<K,V>[] tab; modCount++; if ((tab = table) != null && size > 0) { size = 0; //遍历数组,直接赋值为null for (int i = 0; i < tab.length; ++i) tab[i] = null; } } //是否包含指定的key,实现和get(key)一样 public boolean containsKey(Object key) { return getNode(hash(key), key) != null; } //是否包含某个value值,双层for循环。没一个链表进行对比 public boolean containsValue(Object value) { Node<K,V>[] tab; V v; if ((tab = table) != null && size > 0) { for (int i = 0; i < tab.length; ++i) { for (Node<K,V> e = tab[i]; e != null; e = e.next) { if ((v = e.value) == value || (value != null && value.equals(v))) return true; } } } return false; }
总结
- HashMap是一种散列表,采用(数组 + 链表 + 红黑树)的存储结构;
- HashMap的默认初始容量为16(1<<4),默认装载因子为0.75f,2倍扩容
- 当数组的长度大于64且链表元素的数量大于8时,进行树化
- 当数组的长度小于64且链表元素的数量大于8时,不树化直接扩容
- 当链表元素数量小于6时,进行反树化;
- HashMap是非线程安全的容器。
希望各位大佬帮忙指出错误,大家一起学习一起进步
这篇关于HashMap源码学习的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-27消息中间件底层原理资料详解
- 2024-11-27RocketMQ底层原理资料详解:新手入门教程
- 2024-11-27MQ底层原理资料详解:新手入门教程
- 2024-11-27MQ项目开发资料入门教程
- 2024-11-27RocketMQ源码资料详解:新手入门教程
- 2024-11-27本地多文件上传简易教程
- 2024-11-26消息中间件源码剖析教程
- 2024-11-26JAVA语音识别项目资料的收集与应用
- 2024-11-26Java语音识别项目资料:入门级教程与实战指南
- 2024-11-26SpringAI:Java 开发的智能新利器