Hash Map的原理
说说Java中Hash Map的原理
Hash Map是一种基于哈希表的数据结构,存储键值对格式的数据,通过键的哈希值确定索引在数组上的位置。使用数组+链表(Java8及之后是数组+链表+红黑树)来解决哈希冲突。
使用hashCode()方法计算键的哈希值,并通过indexFor()得到索引位置(Java1.7及之后直接使用公式(n-1)&hash)
Hash Map默认数组长度为16,负载因子为0.75,及当16x0.75=12个位置存在数据时触发扩容(容量x2)将数据重新hash并存储,频繁扩容影响性能
Hash Map的红黑树优化
由于当链表长度大于8之后影响查找性能,因此链表长度大于8之后且数组长度大于64链表变为红黑树,可以将查找的最坏时间复杂度从O(n)降低为O(logn),当数据量减少到6个时红黑树退化为链表,避免因为树操作影响性能
Hash Map成员变量
▼java复制代码// 数组初始容量,有个细节是提到了该初始值必须为2的整数幂,后面讲计算hash值的时候会用到 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 // 最大容量 static final int MAXIMUM_CAPACITY = 1 << 30; // 负载因子 static final float DEFAULT_LOAD_FACTOR = 0.75f; // 数组长度大于64时,转为红黑树的最小链表长度 static final int TREEIFY_THRESHOLD = 8; // 红黑树退化为链表的节点数 static final int UNTREEIFY_THRESHOLD = 6; // 树化时最小数组长度,在此之前会先进行扩容操作 static final int MIN_TREEIFY_CAPACITY = 64;
存储的节点
▼java复制代码// 数组 transient Node<K,V>[] table; // 链表节点 static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; } // 树节点 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; }
节点间继承关系
Hash Map数组初始化
Hash Map采用延迟初始化,即在第一次添加元素时才初始化并且使用resize()方法进行初始化。resize()既用于扩容也用于数组初始化。
▼java复制代码// 构造函数,只设置的负载因子 public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted 该注释其他字段为默认值,例如int为0 } // put方法,即添加元素时初始化 public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } // 只需要看这部分,因为构造函数中的注释,除了负载因子字段都为默认值,此处影响table为null,进入该条件并调用resize() final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; } // 这里table=null,threshold=0都为默认值 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) { // ... } else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; else { // zero initial threshold signifies using defaults // 这里是初始化的过程 newCap = DEFAULT_INITIAL_CAPACITY; // 常量为初始化容量16 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // 下次扩容时数组应该要达到的条件 } threshold = newThr; Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; // 数组初始化 table = newTab; return newTab; }
插入元素
▼java复制代码public V put(K key, V value) { return putVal(hash(key)/*对Key进行hash算法*/, key, value, false, true); } static final int hash(Object key) { int h; // 若key不为null则将key的hashCode高16位和低16位进行异或运算 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16/*无符号右移16位,int是32为,该计算为取高16为*/); } final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; // 判断数组中该索引是否有数据 if ((p = tab[i = (n - 1) & hash/*计算索引,这里回收前面数组默认大小必须为2的整数幂,因为2的整数幂在这里进行运算效果等同于取模操作,且位运算性能更高*/]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; // 判断是否为相同的key if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; // 是否为树节点 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); // 判断是否满足转红黑树的条件 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; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; } final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; // 若数组小于MIN_TREEIFY_CAPACITY则先扩容而不进行树化 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); } }
扩容机制
当Hash Map数组中已经存在(负载因子x当前数组长度。默认情况负载因子为0.75,数组长为16,则阈值为16x0.75=12,即存放第13个元素)时,会将数组长度扩大为两倍,并对原本的数据重新哈希并存储
▼java复制代码final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; // 判断数组大小是否超过阈值 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; } final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; // 此时table为原数组,oldCap为原数组大小 int oldCap = (oldTab == null) ? 0 : oldTab.length; // oldThr为原扩容阈值 int oldThr = threshold; int newCap, newThr = 0; if (oldCap > 0) { if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap << 1/*新数组大小为原数组大小x2*/) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) // 新的扩容阈值为原阈值x2 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); } threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) // 初始化新数组 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; if (oldTab != null) { // 原来的元素重新放入新的HashMap for (int j = 0; j < oldCap; ++j) { // ... } } return newTab; }
评论
问答助学
相关内容
0个评论
全部评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
作者分享
我服啦,老板半个月(算上测试一起)要上一个小程序,十二个模块加后台。要im,要站内信,还有好多个第三方服务。之前开会的时候还说他去了解过敏捷开发,我寻思他只看了名字吧,这也太敏捷了🤣
2
今天又发现ai一个好久之前产出的Bug,而且还有点难注意到。业务是这样的,一个商城的场景,编辑某个商品,对于多规格的商品,目前采用先删除原有的规格名和规格值,并插入新的。但是测试的时候出错了。排查日志发现是根据规格名id查询规格值的时候使用了in,但是没传入任何规格名的id,sql执行出错,向上翻日志发现查询发现是没查到对应的规格名记录(即图中queryByProductId方法),ps关于是否会有规格名在上面有严格校验。于是我去数据库里看了一下,是有数据的,而且用日志中一样的sql,也能直接查到,写了个测试类也能查到,但是接口中就是查不到。谁能想到,ai写的时候先逻辑删除了这条数据,然后紧接着去查了这条数据,当然查不到了😑
2
Spring源码阅读过程中基础知识补齐
3
最近我的GLM Lite订阅马上要过期了,我在纠结到底是续期还是换MiniMax starter。先说一下我的状态吧,目前主要是配合Claude Code在日常工作中处理一些不太复杂的编码任务,目前来说,GLM能满足我的需求,但是最近智谱出了挺多问题的,我偶尔请求的时候会提示什么速率限制(按照官网的描述,订阅套餐是没有请求速率限制的)我也在群里问了,但是他们的回复很慢,甚至都不回复,这也是我有想要换订阅的一个理由吧,虽然我平时没什么问题需要解决,但我还是希望在我有问题的时候有人能给我一个回复。而且高峰期是真的挺慢的。因此我想问一下,MiniMax的使用体验怎么样,有没有人能解答一下的,而且starter的真的很便宜,比GLM Lite按年订阅单月价格还低,不过我看他们的速度应该是相当于GLM高峰期的速度,不过如果其他体验相当加上这个价格我觉得也不是不可以尝试,毕竟一个月而已,不行再换呗😂
2
if (CollectionUtils.isEmpty(userShops)) {
ValidationUtils.isTrue(false, "用户未关联门店");
}
这是今天看到的AI生成的代码,有点好笑,给大家分享一下
7
