Java容器源码攻坚战--第三战:HashMap(一)

mac2022-06-30  108

零、前言:

HashMap怪复杂的,如果一开始就上网上一大堆的HashMap的元素图,也没什么太大意思。 这里从一个小测试开始说起,一步步debug在HashMap里走一走。感觉有时候看源码有点像在风景区看风景,抱着的态度决定你的历程,那些漫步于风景中的人会着眼当前,收获每一个瞬间带给自己的感触。那些苛求踏遍每一份土地,览尽一切风光的人,倒是捉襟见肘,让行程变得劳顿。后者或许览尽风光而无憾,前者虽只览片景却仍收获颇丰,然而这并没有好坏之分,只有对你适合与否。----张风捷特烈 场景:模拟英语字典,有索引类和单词类,索引作为键,单词作为值放入HashMap中 由于HashMap挺大的,本篇只说一下HashMap的插入操作,包括:扩容、链表插入、链表树化。


一、测试HashMap插入

1.索引类:WordIndex--包括单词和页数

这里键的哈希函数直接使用页码

/** * 作者:张风捷特烈 * 时间:2018/10/3 0003:7:35 * 邮箱:1981462002@qq.com * 说明:单词索引类 */ public class WordIndex { /** * 索引出的单词 */ private String word; /** * 单词所在页数 */ private Integer page; public WordIndex(String word, Integer page) { this.word = word; this.page = page; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof WordIndex)) return false; WordIndex wordIndex = (WordIndex) o; return Objects.equals(getWord(), wordIndex.getWord()) && Objects.equals(getPage(), wordIndex.getPage()); } @Override public int hashCode() { return page; } //省略get、set、toString }
2.单词类:Word--包括单词和释意
/** * 作者:张风捷特烈 * 时间:2018/10/3 0003:7:42 * 邮箱:1981462002@qq.com * 说明:单词类 */ public class Word { /** * 单词 */ private String word; /** * 释意 */ private String desc; public Word(String word, String desc) { this.word = word; this.desc = desc; } //省略get、set、toString }
HashMap测试类
/** * 作者:张风捷特烈 * 时间:2018/10/2 0002:19:37 * 邮箱:1981462002@qq.com * 说明:HashMap测试类 */ public class HashMapTest { public static void main(String[] args) { WordIndex act_key = new WordIndex("act", 21); WordIndex arm_key = new WordIndex("arm", 80); WordIndex arise_key = new WordIndex("arise", 80); WordIndex a_key = new WordIndex("a", 1); Word act = new Word("act", "行动"); Word arm = new Word("arm", "胳膊"); Word arise = new Word("arise", "生起"); Word a = new Word("a", "一个"); HashMap<WordIndex, Word> dictionary = new HashMap<>(); dictionary.put(act_key, act); dictionary.put(arm_key, arm); dictionary.put(a_key, a); dictionary.put(arise_key, arise); System.out.println(dictionary); //{WordIndex{word='a', page=1}=Word{word='a', desc='一个'}, // WordIndex{word='act', page=21}=Word{word='act', desc='行动'}, // WordIndex{word='arm', page=80}=Word{word='arm', desc='胳膊'}, // WordIndex{word='arise', page=80}=Word{word='arise', desc='生起'}} } }

二、HashMap分析前置准备

1.成员变量
//可见是一个Node类型的数组,名称是[表] transient Node<K,V>[] table; //当前 HashMap 所能容纳键值对数量的最大值,超过这个值,则需扩容 //threshold = capacity * loadFactor int threshold; //表的最大容量 1 << 30 即2的30次方 //表的容量必须是2的n次方 static final int MAXIMUM_CAPACITY = 1 << 30; //默认容量(16):必须是2的n次方 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 //默认负载因子(0.75)--无参构造时使用 static final float DEFAULT_LOAD_FACTOR = 0.75f; //负载因子 final float loadFactor;
2.链表节点类:可见是一个单链表
static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next;
3.红黑树节点类
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; TreeNode(int hash, K key, V val, Node<K,V> next) { super(hash, key, val, next); } HashMap节点.png

可见TreeNode最终也是继承自Node的

三、HashMap插入第一个元素分析

dictionary.put(act_key, act);

m1:java.util.HashMap#put

添加时调用了putVal方法:见m1-1

* @param key 键:WordIndex{word='act', page=21} * @param value 值:Word{word='act', desc='行动'} public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
m1-1:java.util.HashMap#putVal
* @param hash 键的哈希值------21 * @param key 键 --------------WordIndex{word='act', page=21} * @param value 值 ------------Word{word='act', desc='行动'} * @param onlyIfAbsent true时,不改变已存在的value-----false * @param evict if false, the table is in creation mode.--true * @return previous value, or null if none */ final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { //tab:记录当前表 //n:当前表的长度 Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) //第一次插入为表空时,执行resize扩容返回新表,容量16,见: m2 n = (tab = resize()).length;//此时n=16 //此处p为table[i]的元素 此时i = 15 & 21 = 5 if ((p = tab[i = (n - 1) & hash]) == null)//hash:传入键的hash值 //可以看到添加元素并且没有哈希碰撞时,将元素放入数组的i位 tab[i] = newNode(hash, key, value, null); else {//否则,即哈希冲突了 添加时哈希冲突见:m1-1-2 } ++modCount;//修改次数+1 if (++size > threshold)//元素总数+1后比扩容阀值大时,扩容 resize(); afterNodeInsertion(evict);//插入节点之后:见:m1-1-1 return null;//返回空 }
m1-1-1:可见是为了LinkedHashMap而准备的回调函数,这里都是空实现,所以不管了
// Callbacks to allow LinkedHashMap post-actions void afterNodeAccess(Node<K,V> p) { } void afterNodeInsertion(boolean evict) { } void afterNodeRemoval(Node<K,V> p) { }
m2:java.util.HashMap#resize
final Node<K,V>[] resize() { //变量oldTab记录当前表 Node<K,V>[] oldTab = table; //变量oldCap记录当前表的容量(表为空时长度为0) int oldCap = (oldTab == null) ? 0 : oldTab.length; //oldThr记录当前扩容阀值 int oldThr = threshold; //变量newCap,newThr 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) //将扩容阀值变为2倍 newThr = oldThr << 1; // double threshold } //oldCap=0的时候 else if (oldThr > 0) //旧的扩容阀值大于零,说明并不是初始化 newCap = oldThr; else {//☆☆☆这里是添加第一个元素时扩容初始化的地方 newCap = DEFAULT_INITIAL_CAPACITY;//让新容量变为默认容量(即16) //新的扩容阀值为:默认负载因子*默认默认容量:即(0.75*16=12) 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); } threshold = newThr;//为扩容阀值赋值 @SuppressWarnings({"rawtypes","unchecked"}) //以新的容量创建新表 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab;//将临时新表赋给HashMap的表:插播一幅图 if (oldTab != null) {//旧表非空--表示不是初始化 //暂略...详见:m2-1 } return newTab;//返回新表 } HashMap初始化.png

二、插入分析

在索引为5的地方插入了一个链表节点,索引位置由:[表容量-1 & 添加键的哈希值]决定 节点:hash=21----key:WordIndex{word='act', page=21}----value:Word{word='act', desc='行动'}----next为空

HashMap插入第一个元素.png

第二个元素hash值80 : 15 & 80 = 0 所以插入第0位 节点:hash=80----key:WordIndex{word='arm', page=80}----value:Word{word='arm', desc='胳膊'}----next为空

HashMap插入第二个元素.png

第三个元素hash值1 : 1 & 80 = 1 所以插入第1位 节点:hash=1----key:WordIndex{word='1', page=1}----value:Word{word='1', desc='一个'}----next为空

HashMap插入第三个元素.png

重点来了:插入第四个元素arise,它键的hash值和第二个元素:arm都是80,也就说明它们在同一页

HashMap插入第四个元素.png
m1-1-2:插入时哈希冲突处理
Node<K,V> e; K k; //此处p为table[i]的元素,也就是table[15&80]=table[0]:即单词--arm //变量k记录p节点的key,e记录当前节点 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p;//传入键的hash值等于节点的hash字段,并且两者key不同 else if (p instanceof TreeNode)//如果是树,按树的插入 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else {//否则,按链表的插入, //关于bin:就像数组里放了一个个垃圾桶(链表),来一个哈希冲突的就往里扔一个, //所以binCount就是链表的容量 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; }

节点内的单词是这样的

HashMap插入第四个.png
关于:i = (n - 1) & hash 有点想不通这是干嘛用的,然后查了一下
当 n = 2^y 时,x % n = (n - 1) & x
比如:
int x = 67444; int i1 = 255 & x;//===>67444 % 255 int i2 = x % 256;//===>67444 % 256 System.out.println(i1);//166 System.out.println(i2);//166 System.out.println(i1 == i2);

又想到HashMap中多次指出:表的长度必须是2的次方,所以两者结合豁然开朗: 其中n代表表的长度,是2的次方,满足当 n = 2^y 时,x % n = (n - 1) & x,所以:

i = (n - 1) & hash 即 i = n % hash

至于为什么如此:

1.添加时将[key的hash值]亦或[key的hash值无符号右移16位]

static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }

2.使用取模为了使元素分布均匀,并且&运算符要比%运算符高效

三、链表的树化

// 链表转为红黑树的阀值 static final int TREEIFY_THRESHOLD = 8; // 转回链表阀值 static final int UNTREEIFY_THRESHOLD = 6; // map总容量对转为红黑树的限定阀值 static final int MIN_TREEIFY_CAPACITY = 64; 比如总容量只有40,及时哈希碰撞非常集中,有条链表已经长30了,也不能转为红黑树。 final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; //如果表的长度 < 64 (即树化的最小容量限制) if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize();//扩容 //容量容量大于64等于时,满足树化条件 //临时变量index记录插入元素对应的链表所在的数组索引 //临时变量e记录插入元素对应的链表表头元素 else if ((e = tab[index = (n - 1) & hash]) != null) { TreeNode<K,V> hd = null, tl = null; do { //根据头结点e实例化出红黑树节点p TreeNode<K,V> p = replacementTreeNode(e, null); if (tl == null)//只有第一次才会走这里 hd = p;//临时变量hd记录红黑树节点p else {//将链表Node一个一个更换为红黑树的TreeNode p.prev = tl;//转换过的TreeNode的前一位是tl(即上一个换过的TreeNode) tl.next = p;//同理 } tl = p;//临时变量tl记录红黑树节点p } while ((e = e.next) != null); if ((tab[index] = hd) != null)//至此该链表所有Node都换成了TreeNode,但还是链表 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; //此处声明两个TreeNode类型变量:x和next ,并将x赋值为当前节点 //结束条件:x != null 完成一次循环执行x = next for (TreeNode<K,V> x = this, next; x != null; x = next) { //将x的下一节点赋值给next变量 next = (TreeNode<K,V>)x.next; //将x的左右置空 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;//节点hash值 Class<?> kc = null; //从根节点开始,对当前节点进行插入,此循环用break退出 for (TreeNode<K,V> p = root;;) { int dir, ph; K pk = p.key; //比较h与父节点的大小 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); //xp记录x的父亲 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.本文成长记录及勘误表
项目源码日期备注V0.1--无2018-10-2Java容器源码攻坚战--第三战:HashMapV0.2--无--
2.更多关于我
笔名QQ微信爱好张风捷特烈1981462002zdl1994328语言我的github我的简书我的个人网站
3.声明

1----本文由张风捷特烈原创,转载请注明 2----欢迎广大编程爱好者共同交流 3---个人能力有限,如有不正之处欢迎大家批评指证,必定虚心改正 4----看到这里,我在此感谢你的喜欢与支持

转载于:https://www.cnblogs.com/toly-top/p/9781854.html

相关资源:JAVA上百实例源码以及开源项目
最新回复(0)