hashmap分析
3.HashMap
组成关系
3.1在哪个包下
package java.util;
3.2类的继承关系
Cloneable
空接口,表示可以克隆,创建并返回HashMap对象的一个副本;Serializable
序列化接口,属于标记性接口,HashMap对象可以被序列化和反序列化;AbstractMap
父类提供了Map实现接口,以最大限度地减少实现此接口所需的工作;
3.2成员变量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 //默认初始容量为16 (1左移4位)。 static final int MAXIMUM_CAPACITY = 1 << 30; //最大容量。 static final float DEFAULT_LOAD_FACTOR = 0.75f; //构造函数中未进行指定的使用的一个负载因子。 static final int TREEIFY_THRESHOLD = 8; //链表节点转换为红黑树节点的一个阈值。 static final int UNTREEIFY_THRESHOLD = 6; //红黑树节点转为链表节点的阈值。 static final int MIN_TREEIFY_CAPACITY = 64; //树化的最小容量为64,也就是哈希表的长度。
3.3为什么默认加载因子为0.75f
为了提高空间利用率和增加查询效率的折中。主要是泊松分布,0.75的话碰撞最小。 如果负载因子比较小,因为阈值等于哈希表的长度*负载因子。(阈值是哈希表中可以存储元素的个数)。哈希表能存的元素个数就更少,那么相应的发生哈希冲突的概率也就降低了,所以查询效率也就提高。但是由于增加相同数量的元素,需要的哈希桶的个数就要进行增加。于是就多次调用扩容方法,扩容会重构哈希结构,这个过程很慢,空间利用率就降低了。为了在时间和空间上有一个折中的结果,满足泊松分布取0.75是一个比较理想的取值。
3.4为什么哈希桶中的节点超过8才转为红黑树?
将链表转为红黑树是为了提高查询的效率,但是这样也带来了一些问题,树节点的大小是普通节点的2倍。 在节点数量较低时,维护红黑树结构的成本是不低于查询成本的,所以此时不值得进行转换。 源码中也有解释 : 在使用分布良好的hashcode时,很少使用红黑树结构,理想情况下,在随机哈希码下,箱子中节点的频率服从泊松分布,从下面源码的概率值可以看出,一个哈希桶中链表长度达到8个元素的概率为0.00000006,这几乎是一个不可能事件。因此选择8,不是随便就决定的,而是根据概率统计决定的。还是得学好数学啊。
* Because TreeNodes are about twice the size of regular nodes, we * use them only when bins contain enough nodes to warrant use * (see TREEIFY_THRESHOLD). And when they become too small (due to * removal or resizing) they are converted back to plain bins. In * usages with well-distributed user hashCodes, tree bins are * rarely used. Ideally, under random hashCodes, the frequency of * nodes in bins follows a Poisson distribution * (http://en.wikipedia.org/wiki/Poisson_distribution) with a * parameter of about 0.5 on average for the default resizing * threshold of 0.75, although with a large variance because of * resizing granularity. Ignoring variance, the expected * occurrences of list size k are (exp(-0.5) * pow(0.5, k) / * factorial(k)). The first values are: * * 0: 0.60653066 * 1: 0.30326533 * 2: 0.07581633 * 3: 0.01263606 * 4: 0.00157952 * 5: 0.00015795 * 6: 0.00001316 * 7: 0.00000094 * 8: 0.00000006 * more: less than 1 in ten million
3.5构造方法
3.5.1指定初始容量和指定负载因子
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); } //返回给定目标容量的 2 次方。 static final int tableSizeFor(int cap) { int n = cap - 1; //对传入的cap进行减1操作,是为了防止cap已经是2的幂次方。 n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; } 假设输入的值为cap=10 int n = cap - 1;//cap=10 n=9 n |= n >>> 1; 00000000 00000000 00000000 00001001 //9 00000000 00000000 00000000 00000100 //9 >>> 1 = 4 ------------------------------------------------- 00000000 00000000 00000000 00001101 //9 | (9 >>> 1) = 13 n |= n >>> 2;//n = 13 00000000 00000000 00000000 00001101 //13 00000000 00000000 00000000 00000011 //13 >>> 2 = 3 ------------------------------------------------- 00000000 00000000 00000000 00001111 //13 | (13 >>> 2) = 15 //接下去的移位也是类似,移4位、8位、16位,保证在2 ^ 30内能达到目标即可 //此时已经得到我们想要的结果,所以就不继续下去了。这里我做下补充,如果在继续15 | 15>>>4 得到的结果还是15,最后的几步就相当于是没有变化,最后按下面的巧妙的方法得到的值就是16了。不得不说真的很巧妙。 //判断最终结果,总之就是一个很巧妙的方法 (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
3.5.2指定一个初始容量和默认加载因子【推荐使用】
public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } //这里默认会去调用3.5.3中的构造方法
3.5.3空参构造方法【经常使用】
public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // 默认16 和 0.75f }
3.5.4构造参数是map的构造方法
public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; //默认为0.75f putMapEntries(m, false); } //---------------------------------------------------------------------------- final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) { int s = m.size(); if (s > 0) { if (table == null) { // pre-size 如果原来的同还没有初始化 float ft = ((float)s / loadFactor) + 1.0F; //计算需要的桶的大小 int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY); if (t > threshold) //如果计算出来的大于原来的计算出来的扩容大小,则把新计算出来的大小进行赋值,其实就是重新赋值 threshold = tableSizeFor(t); } else if (s > threshold) //判断当前的桶的大小可不可以存下m的所有的元素 resize(); //不能存下则从新扩容 for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) { //遍历拷贝元素 K key = e.getKey(); V value = e.getValue(); putVal(hash(key), key, value, false, evict); } } }
3.6 hash()方法
该方法是hashmap用来定位元素存储的位置的一个重要方法。我们希望HashMap里面的元素位置尽量分布均匀,因为如果很多元素都在某一个位置,那么我们定位到哈希桶后,还需要继续遍历该位置下的链表或者红黑树,导致效率降低,所以应尽量使得每个哈希桶内的元素只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,而不用遍历链表或者红黑树,大大优化了查询的效率。
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
我来继续讲解这个return里面的三目运算。这里的hashCode()是直接调用的本地方法,没有进行重写,得到的h再与h无符号右移16的结果进行异或运算,让高位参与运算,会让得到的hashcode的值更加的散列。
但是为什么进行无符号右移16呢?
// >>> 无符号右移,忽略符号位,空位都以0补齐 // 按二进制形式把所有的数字向右移动对应位数,低位移出(舍弃),高位的空位补零。对于正数来说和带符号右移相同,对于负数来说不同。
为什么要采用^,不是&或者|呢?
我们来看看这三种情况:
使用异或就会让返回的hashCode的值更加的分散,使用与运算(&)会让hashCode的值偏向于0,或运算(|)会让hashCode的值偏向于1。我们的目的不就是获得一个比较分散的值吗? 那肯定使用亦或呀(o゚▽゚)o
3.7 put()方法
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } //----------------------上面的主要是调用这个方法----------------------------------------- // onlyIfAbsent:true代表不更改现有的值,否则覆盖;默认是覆盖 // evict:如果为false表示table为创建状态,默认是非创建状态 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; //存储table的引用 Node<K,V> p; int n, i; //n是桶的个数,i是桶的下标 if ((tab = table) == null || (n = tab.length) == 0) //如果是第一次插入,则一定为null或者桶的个数为0,对table进行初始化 n = (tab = resize()).length; //初始化完成后赋值n if ((p = tab[i = (n - 1) & hash]) == null) //将目标桶下第一个元素赋值给p,若p == null,则代表目标桶还未存储过元素,直接初始化桶并插入即可 tab[i] = newNode(hash, key, value, null); else { ////否则说明发生哈希冲突 Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //如果第一个桶的元素的key是重复的,那么就使用value覆盖原来的值 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) { //若遇到null,直接插入节点 p.next = newNode(hash, key, value, null); //判断此时链表长度是否达到转化红黑树的阈值 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st 从-1开始所以这里进行-1 7 treeifyBin(tab, hash); //若是则进行转化 break; } //若找到key相同的节点,则进行覆盖 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) //如果此时表中元素个数大于阈值threshold,则进行扩容, resize(); afterNodeInsertion(evict); //插入后回调 return null; }
整个过程的流程如下图
1.先通过哈希值计算出key映射到哪个桶; 2.如果桶上没有碰撞冲突,则直接插入; 3.如果遇上了冲突,则需要冲突处理: (1)如果该桶使用红黑树处理冲突,则调用红黑树的方法插入数据; (2)否则采用传统的链式方法插入。如果链的长度达到临界值,则把链转变为红黑树; 4.如果桶中存在重复的key,则用新值替换老值并返回老值; 5.如果size大于阈值threshold,则进行扩容。
3.8 remove()方法
public V remove(Object key) { Node<K,V> e; return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value; } //---------------------------------------------------------------------------- // matchValue 如果为true,表示只在值相等的时候进行删除,但是默认是false。 // movable 如果为false,表示删除的时候不移动其他节点,默认是true 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; //找目标节点 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; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) node = p; //如果目标桶中第一个元素为要删除的目标元素,则赋值给node else if ((e = p.next) != null) { //判断该桶下的其他元素 if (p instanceof TreeNode) //若元素为树节点,则调用方法去树中查询,查询结果赋值给node 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; //找到节点则赋值给node并break break; } p = e; } while ((e = e.next) != null); } } //接下来对于找到的节点从哈希表中删除 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; //无目标元素则返回null }
流程如下
1.先找到元素的目标桶,即存储位置; 2。如果该桶下的存储结构是链表的话,则遍历找到并删除即可; 3。如果是红黑树结构,则遍历找到并删除,如果此时节点数少于红黑树转链表的阈值6时,调用untreeify方法将红黑树转化成链表
3.9 get()方法
public V get(Object key) { Node<K,V> e; 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; 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); do { //不是的话就是链表遍历获取 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; //无目标元素则返回null }
3.10 treeifyBin()链表转为红黑树
这是在3.7 的put方法中的putval()的方法,如果桶里面的链表的长度大于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) //传入的tab肯定是不是null,直接看后面的,当桶的个数不足64的时候,选择的是扩容。 resize(); //桶的数量大于64了,则进行转为红黑树 else if ((e = tab[index = (n - 1) & hash]) != null) { //使用e定位到链表的第一个节点 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); //不难发现上述while虽然是一颗红黑树,但是是链状的 if ((tab[index] = hd) != null) //将红黑树头节点放进桶中 hd.treeify(tab); //将上述链状红黑树进行平衡处理 } } //----------------------------将原来的节点转为数节点---------------- TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) { return new TreeNode<>(p.hash, p.key, p.value, next); } //-----------------------------平衡处理---------------------------- final void treeify(Node<K,V>[] tab) { TreeNode<K,V> root = null; for (TreeNode<K,V> x = this, next; x != null; x = next) { next = (TreeNode<K,V>)x.next; x.left = x.right = null; if (root == null) { x.parent = null; x.red = false; root = x; } else { K k = x.key; int h = x.hash; Class<?> kc = null; for (TreeNode<K,V> p = root;;) { int dir, ph; K pk = p.key; if ((ph = p.hash) > h) dir = -1; else if (ph < h) dir = 1; else if ((kc == null && (kc = comparableClassFor(k)) == null) || (dir = compareComparables(kc, k, pk)) == 0) dir = tieBreakOrder(k, pk); TreeNode<K,V> xp = p; if ((p = (dir <= 0) ? p.left : p.right) == null) { x.parent = xp; if (dir <= 0) xp.left = x; else xp.right = x; root = balanceInsertion(root, x); break; } } } } moveRootToFront(tab, root); }
总体的步骤就是:如下
1.根据哈希表中元素个数确定是扩容还是树形化; 2.如果是树形化遍历桶中的元素,创建相同个数的树形节点,复制内容,建立起联系; 3.然后让桶中的第一个元素指向新创建的树根节点,替换桶的链表内容为树形化内容; 4.最后处理红黑树,红黑树中比较大小的依据为hash值。
3.11 reseize()方法【扩容】
hashmap的扩容,每次扩容都会将表的大小扩容为原来的2倍
3.11.1扩容的时机
1.当容器中的元素数量大于阈值的时候就会触发扩容。(阈值就是表的大小乘负载因子)
2.当某一个桶中的元素数量大于8的时候,并且哈希表的大小小于64的时候,触发扩容。
final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; //存下原来的hash表 int oldCap = (oldTab == null) ? 0 : oldTab.length; //记录原hash表的大小 int oldThr = threshold; //记录原来的阈值 int newCap, newThr = 0; //声明扩容后的大小和阈值 if (oldCap > 0) { //如果原哈希表大小大于0 if (oldCap >= MAXIMUM_CAPACITY) { //判断其是否超过了上限 threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && //判断扩容为原先两倍后有没有超出上限 oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } //原哈希表大小为0,但是阈值大于0,此时便是之前提到的修正位置 else if (oldThr > 0) newCap = oldThr; //赋值给新大小 else { //否则使用默认大小16和默认加载因子0.75计算出的阈值12 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; if ((e = oldTab[j]) != null) { //当前桶中有元素,并赋值给节点e 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 否则就是链表 Node<K,V> loHead = null, loTail = null; //声明两条链表l和h Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { //用循环将所有节点取出并放入l或h中 next = e.next; if ((e.hash & oldCap) == 0) { //重点方法,需比较一位二进制为即可判断它在新哈希表中的目标桶位置 if (loTail == null) //为0代表位置不变 loHead = e; else loTail.next = e; loTail = e; } else { //为1则代表变为 原位置+旧容量 if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { //将两条链表分别放进哈希桶中 loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; }
这里再简单描述一下关于桶中元素为红黑树的情况:
else if (e instanceof TreeNode) //如果是红黑树结构,则调用相关方法把树分开 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
split
方法是先将当前这棵树按照上述位运算结论,分解成类似上述l、h两条链表的l、h两颗红黑树,然后分别对两颗红黑树进行节点数量判断,若小于UNTREEIFY_THRESHOLD
(红黑树转链表的阈值),则将其转变成链表,然后分别存储新哈希表的目标桶中。
3.12其他注意事项
hashmap初始化,推荐使用指定集合初始值的大小的构造方法,目的是为了减少扩容,扩容操作是十分耗时的。如果暂时不知道扩容的大小是多大,就设置为默认的16就好ヾ(๑╹◡╹)ノ"。
如果知道有多大,那么就设置为下面计算出来的公式的大小
这里的加载因子为0.75f,举个例子如果你想存100,那么就可以设置大小为100/0.75 约等于133.3,在进行+1,就是134.3,取整后为135
本文来自博客园,作者:程序员鲜豪,转载请注明原文链接:https://www.cnblogs.com/hg-blogs/p/16032746.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器