本文将分析put(),resize(),get()和remove()方法的源码

putval()方法

  大致步骤:计算key的hash值;根据hash值计算数组下标;判断下标处是否有节点,无节点则直接插入,有则根据是链表还是红黑树进行对应操作。
  这里需要注意的是如果插入链表后长度达到了8,且table数组长度达到64,则会对链表进行树化转为红黑树;如果长度达到了8,且table数组长度小于64,则会进行resize()对table数组扩容,目的是将链表各节点rehash分成高低位两个链表以减少链表长度。
 1 // 内部节点数组
2 transient Node<K,V>[] table;
3
4 // 对外界提供的
5 public V put(K key, V value) {
6 return putVal(hash(key), key, value, false, true);
7 }
8 // 内部实现
9 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
10 boolean evict) {
11 Node<K,V>[] tab; Node<K,V> p; int n, i;
12 // 如果节点数组未初始化或为空,则进行初始化操作
13 if ((tab = table) == null || (n = tab.length) == 0)
14 n = (tab = resize()).length;
15 // 如果根据hash值得到的数组对应位置还没有元素,则直接插入
16 if ((p = tab[i = (n - 1) & hash]) == null)
17 tab[i] = newNode(hash, key, value, null);
18 // 如果有元素
19 else {
20 // hash冲突
21 Node<K,V> e; K k;
22 // 如果hash值和key值相同,则直接替换原值
23 if (p.hash == hash &&
24 ((k = p.key) == key || (key != null && key.equals(k))))
25 e = p;
26 // 如果节点已经是树节点,进行树模式的插入
27 else if (p instanceof TreeNode)
28 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
29 else {
30 // 如果还是链表,则遍历链表插入数据
31 for (int binCount = 0; ; ++binCount) {
32 // 这里采用的是尾插法,如果遍历链表过程中没发现key相同节点,则在链表尾部新建节点
33 if ((e = p.next) == null) {
34 p.next = newNode(hash, key, value, null);
35 // 如果链表的长度达到了8,且数组cap大小>=64则转为红黑树
36 // 如果链表长度达到了8,但数组cap大小<64则resize()扩容
37 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
38 treeifyBin(tab, hash);
39 break;
40 }
41 // 如果存在key相同的节点,则不插入,退出循环
42 if (e.hash == hash &&
43 ((k = e.key) == key || (key != null && key.equals(k))))
44 break;
45 p = e;
46 }
47 }
48 if (e != null) { // existing mapping for key
49 V oldValue = e.value;
50 if (!onlyIfAbsent || oldValue == null)
51 e.value = value;
52 afterNodeAccess(e);
53 return oldValue;
54 }
55 }
56 ++modCount;
57 // 数量超过阈值,进行一次扩容操作
58 if (++size > threshold)
59 resize();
60 afterNodeInsertion(evict);// 回调
61 return null;
62 }

resize()方法

大致步骤:
  判度当前容量cap和当前阈值thr是否符合扩容条件(翻倍扩容后不超过int最大值)并更新cap和thr的值;
  如果可以扩容,则根据新cap新建一个数组;
  对原数组进行遍历,复制元素到新数组,有三种情况:无后继节点;链表;红黑树。具体复制细节可以参考作者另一篇博客:https://www.cnblogs.com/ningbing/p/14599571.html
 1 final Node<K,V>[] resize() {
2 // 获取原有table
3 Node<K,V>[] oldTab = table;
4 int oldCap = (oldTab == null) ? 0 : oldTab.length;
5 int oldThr = threshold;
6 // 新容量、新阈值
7 int newCap, newThr = 0;
8 if (oldCap > 0) {
9 // 如果原有容量超过设定最大容量,则不进行扩容
10 if (oldCap >= MAXIMUM_CAPACITY) {
11 threshold = Integer.MAX_VALUE;
12 return oldTab;
13 }
14 // 如果原容量翻倍后不超过最大容量且原容量超过16,则进行翻倍扩容
15 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
16 oldCap >= DEFAULT_INITIAL_CAPACITY)
17 newThr = oldThr << 1; // double threshold
18 }
19 else if (oldThr > 0) // 使用阈值初始化容量,对应的是初始化hashmap带了容量cpacity参数。
20 newCap = oldThr;
21 else { // zero initial threshold signifies using defaults
22 // 原容量和阈值都<=0,则用默认值初始化,默认容量16,负载因子0.75,阈值12,对应的是hashmap没带参数初始化。
23 newCap = DEFAULT_INITIAL_CAPACITY;
24 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
25 }
26 if (newThr == 0) {
27 // 如果新阈值为0则赋值为新容量*负载因子(默认是0.75)
28 float ft = (float)newCap * loadFactor;
29 newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
30 (int)ft : Integer.MAX_VALUE);
31 }
32 threshold = newThr;// 更新阈值
33 // 基于新容量重新实例化一个node数组
34 @SuppressWarnings({"rawtypes","unchecked"})
35 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
36 table = newTab;// 更新数组
37 if (oldTab != null) {
38 // 遍历数组的每个节点元素
39 for (int j = 0; j < oldCap; ++j) {
40 Node<K,V> e;
41 // 如果节点不为空
42 if ((e = oldTab[j]) != null) {
43 oldTab[j] = null;// 将原数组节点指向空
44 // case1:节点只有一个元素,直接根据hash值计算新位置
45 if (e.next == null)
46 newTab[e.hash & (newCap - 1)] = e;
47 // case2:节点为红黑树节点,进行红黑树的复制操作
48 else if (e instanceof TreeNode)
49 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
50 // case3:节点为链表节点,进行链表的赋值操作
51 else { // preserve order
52 // 低位Node链表头节点和尾节点
53 Node<K,V> loHead = null, loTail = null;
54 // 高位Node链表头节点和尾节点
55 Node<K,V> hiHead = null, hiTail = null;
56 Node<K,V> next;
57 // 遍历原链表,拆分成低位链表和高位链表
58 do {
59 next = e.next;
60 // 如果是在原位置,则加入低位链表
61 if ((e.hash & oldCap) == 0) {
62 if (loTail == null)
63 loHead = e;
64 else
65 loTail.next = e;
66 loTail = e;
67 }
68 else {
69 // 如果不在原位置,加入高位链表
70 if (hiTail == null)
71 hiHead = e;
72 else
73 hiTail.next = e;
74 hiTail = e;
75 }
76 } while ((e = next) != null);
77 // 如果低位链表不为空
78 if (loTail != null) {
79 // 尾部节点赋空并将头部节点放入数组指定位置
80 loTail.next = null;
81 newTab[j] = loHead;
82 }
83 // 如果高位链表不为空
84 if (hiTail != null) {
85 // 尾部节点赋空并将头部节点放入数组指定位置
86 hiTail.next = null;
87 newTab[j + oldCap] = hiHead;
88 }
89 }
90 }
91 }
92 }
93 return newTab;
94 }

TreeNode split()方法:map扩容时红黑树的拆分+重新存储

 1 final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
2 // 获取自身树节点
3 TreeNode<K,V> b = this;
4 // Relink into lo and hi lists, preserving order
5 // 低位TreeNode链表的头尾节点
6 TreeNode<K,V> loHead = null, loTail = null;
7 // 高位TreeNode链表的头尾节点
8 TreeNode<K,V> hiHead = null, hiTail = null;
9 // 低位链表节点数量、高位链表节点数量
10 int lc = 0, hc = 0;
11 for (TreeNode<K,V> e = b, next; e != null; e = next) {
12 next = (TreeNode<K,V>)e.next;
13 // 这步操作不是多余的,在e为低位或高位链表最终尾节点时起到赋空作用
14 e.next = null;
15 // 如果仍然在原位置,则加入低位链表
16 if ((e.hash & bit) == 0) {
17 if ((e.prev = loTail) == null)
18 loHead = e;
19 else
20 loTail.next = e;
21 loTail = e;
22 ++lc;//低位链表数量+1
23 }
24 else {
25 // 如果是在新的位置(原索引值+oldcap),加入高位链表
26 if ((e.prev = hiTail) == null)
27 hiHead = e;
28 else
29 hiTail.next = e;
30 hiTail = e;
31 ++hc;// 高位链表数量+1
32 }
33 }
34 // 低位链表不为空
35 if (loHead != null) {
36 // 低位链表数量不超过6,则深拷贝低位链表并将新链表头部放入数组
37 if (lc <= UNTREEIFY_THRESHOLD)
38 tab[index] = loHead.untreeify(map);
39 else {
40 tab[index] = loHead;
41 // 如果高位链表为空,说明全部元素都在低位链表中,因为原链表已经是树化的了,所以不用再转为红黑树
42 if (hiHead != null) // (else is already treeified)
43 loHead.treeify(tab);
44 }
45 }
46 // 高位链表不为空
47 if (hiHead != null) {
48 // 高位链表数量不超过6,则深拷贝高位链表并将新链表头部放入数组
49 if (hc <= UNTREEIFY_THRESHOLD)
50 tab[index + bit] = hiHead.untreeify(map);
51 else {
52 tab[index + bit] = hiHead;
53 // 如果低位链表为空,说明全部元素都在高位链表中,因为原链表已经是树化的了,所以不用再转为红黑树
54 if (loHead != null)
55 hiHead.treeify(tab);
56 }
57 }
58 }

get()方法:

大致步骤:
  根据key的hashcode计算出hash值(高16位异或低16位填入低16位);
  然后将hash值&(cap-1)计算出节点数组下标;
  如果没有元素,返回null,有元素则查找链表或查找红黑树判断是否有equals的节点。
 1 public V get(Object key) {
2 Node<K,V> e;
3 return (e = getNode(hash(key), key)) == null ? null : e.value;
4 }
5
6 final Node<K,V> getNode(int hash, Object key) {
7 Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
8 // 如果节点数组不为空且数组长度不为0且hash值计算出下标有元素,则继续判断
9 if ((tab = table) != null && (n = tab.length) > 0 &&
10 (first = tab[(n - 1) & hash]) != null) {
11 // 如果第一个节点就是要找的,直接返回
12 if (first.hash == hash && // always check first node
13 ((k = first.key) == key || (key != null && key.equals(k))))
14 return first;
15 // 后继有节点
16 if ((e = first.next) != null) {
17 // 如果是树,则根据hash值和key对红黑树查找
18 if (first instanceof TreeNode)
19 return ((TreeNode<K,V>)first).getTreeNode(hash, key);
20 // 如果是链表,则遍历各个节点
21 do {
22 if (e.hash == hash &&
23 ((k = e.key) == key || (key != null && key.equals(k))))
24 return e;
25 } while ((e = e.next) != null);
26 }
27 }
28 return null;
29 }
30
31 // 树的查找
32 final TreeNode<K,V> getTreeNode(int h, Object k) {
33 // 有根节点从根节点开始找,没根节点从first节点开始找
34 return ((parent != null) ? root() : this).find(h, k, null);
35 }
36 // 红黑树查找
37 final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
38 // 获取调用的节点(根节点或first节点)
39 TreeNode<K,V> p = this;
40 do {
41 int ph, dir; K pk;
42 TreeNode<K,V> pl = p.left, pr = p.right, q;
43 // 当前节点hash值比待找key的hash值大,则进入左子树
44 if ((ph = p.hash) > h)
45 p = pl;
46 // 当前节点hash值比待找key的hash值小,则进入右子树
47 else if (ph < h)
48 p = pr;
49 // 相同,则比较当前节点是否是待找节点
50 else if ((pk = p.key) == k || (k != null && k.equals(pk)))
51 return p;
52 else if (pl == null)
53 p = pr;
54 else if (pr == null)
55 p = pl;
56 else if ((kc != null ||
57 (kc = comparableClassFor(k)) != null) &&
58 (dir = compareComparables(kc, k, pk)) != 0)
59 p = (dir < 0) ? pl : pr;
60 else if ((q = pr.find(h, k, kc)) != null)
61 return q;
62 else
63 p = pl;
64 } while (p != null);
65 return null;
66 }

remove()方法:

大致步骤和get()方法类似:
  根据key的hashcode计算出hash值(高16位异或低16位填入低16位);
  然后将hash值&(cap-1)计算出节点数组下标;
  如果没有元素,返回null,有元素则查找链表或查找红黑树判断是否有equals的节点;
  如果找到了节点再根据节点类型和位置执行对应删除操作。
 1 public V remove(Object key) {
2 Node<K,V> e;
3 // 调用removeNode方法进行删除
4 return (e = removeNode(hash(key), key, null, false, true)) == null ?
5 null : e.value;
6 }
7 /**
8 * @param hash hash for key
9 * @param key the key
10 * @param value the value to match if matchValue, else ignored
11 * @param matchValue if true only remove if value is equal (如果为true:只有value也相同才移除key相同的节点)
12 * @param movable if false do not move other nodes while removing (如果为false:在删除节点时不能移除其它节点)
13 * @return the node, or null if none
14 */
15 final Node<K,V> removeNode(int hash, Object key, Object value,
16 boolean matchValue, boolean movable) {
17 // p是要删除节点的父节点(如果是链表结构)
18 Node<K,V>[] tab; Node<K,V> p; int n, index;
19 // 如果table数组不为空且hash对应索引位置有元素
20 if ((tab = table) != null && (n = tab.length) > 0 &&
21 (p = tab[index = (n - 1) & hash]) != null) {
22 Node<K,V> node = null, e; K k; V v;
23 // 如果第一个node的hash值和key都与输入参数相同,则为要找的目标
24 // 先比较hash值是为了先用简单条件过滤,equals方法的复杂度要比hash值大多了
25 if (p.hash == hash &&
26 ((k = p.key) == key || (key != null && key.equals(k))))
27 node = p;
28 // 后继有节点
29 else if ((e = p.next) != null) {
30 // 如果是树化的,通过hash值和key对红黑树进行查找
31 if (p instanceof TreeNode)
32 node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
33 // 如果是链表,则遍历各个节点
34 else {
35 do {
36 if (e.hash == hash &&
37 ((k = e.key) == key ||
38 (key != null && key.equals(k)))) {
39 node = e;
40 break;
41 }
42 p = e;
43 } while ((e = e.next) != null);
44 }
45 }
46 // 如果找到了待删除的节点(node不为null,存在)
47 if (node != null && (!matchValue || (v = node.value) == value ||
48 (value != null && value.equals(v)))) {
49 // 如果是树化的,删除红黑树节点
50 if (node instanceof TreeNode)
51 ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
52 // node=p,说明首节点就是待找节点
53 else if (node == p)
54 tab[index] = node.next;
55 // 将待找节点node的父节点p指向node下一个节点。之后node没有任何对象指向它,会被垃圾回收器回收
56 else
57 p.next = node.next;
58 ++modCount;
59 --size;
60 afterNodeRemoval(node);
61 return node;
62 }
63 }
64 return null;
65 }

HashMap方法源码分析的更多相关文章

  1. Java split方法源码分析

    Java split方法源码分析 public String[] split(CharSequence input [, int limit]) { int index = 0; // 指针 bool ...

  2. invalidate和requestLayout方法源码分析

    invalidate方法源码分析 在之前分析View的绘制流程中,最后都有调用一个叫invalidate的方法,这个方法是啥玩意?我们来看一下View类中invalidate系列方法的源码(ViewG ...

  3. Linq分组操作之GroupBy,GroupJoin扩展方法源码分析

    Linq分组操作之GroupBy,GroupJoin扩展方法源码分析 一. GroupBy 解释: 根据指定的键选择器函数对序列中的元素进行分组,并且从每个组及其键中创建结果值. 查询表达式: var ...

  4. HashMap的源码分析与实现 伸缩性角度看hashmap的不足

    本文介绍 1.hashmap的概念 2.hashmap的源码分析 3.hashmap的手写实现 4.伸缩性角度看hashmap的不足 一.HashMap的概念 HashMap可以将其拆分为Hash散列 ...

  5. HashMap的源码分析

    hashMap的底层实现是 数组+链表 的数据结构,数组是一个Entry<K,V>[] 的键值对对象数组,在数组的每个索引上存储的是包含Entry的节点对象,每个Entry对象是一个单链表 ...

  6. Java——HashMap底层源码分析

    1.简介 HashMap 根据键的 hashCode 值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的. HashMap 最多只允许一条记录的key为 nu ...

  7. Java中HashMap的源码分析

    先来回顾一下Map类中常用实现类的区别: HashMap:底层实现是哈希表+链表,在JDK8中,当链表长度大于8时转换为红黑树,线程不安全,效率高,允许key或value为null HashTable ...

  8. jQuery实现DOM加载方法源码分析

    传统的判断dom加载的方法 使用 dom0级 onload事件来进行触发所有浏览器都支持在最初是很流行的写法 我们都熟悉这种写法: window.onload=function(){ ... }  但 ...

  9. jQuery.extend()方法和jQuery.fn.extend()方法源码分析

    这两个方法用的是相同的代码,一个用于给jQuery对象或者普通对象合并属性和方法一个是针对jQuery对象的实例,对于基本用法举几个例子: html代码如下: <!doctype html> ...

随机推荐

  1. 面试现场:说说char 和 varchar的区别你了解多少?

    Hi,大家好!我是白日梦!本文是MySQL专题的第 26 篇. 下文还是白日梦以自导自演的方式,围绕"说说char 和 varchar的区别你了解多少?"展开本话题.看看你能抗到第 ...

  2. java 动态规划解决最大连续子数列和

    很多动态规划算法非常像数学中的递推.我们如果能找到一个合适的递推公式,就能很容易的解决问题.我们用dp[n]表示以第n个数结尾的最大连续子序列的和,这里第n个数必须在子序列中.于是存在以下递推公式: ...

  3. python爬取三国演义的所有章节储存到本地文件中

    #爬取三国演义的全部章节 2 3 import urllib 4 import urllib.request 5 import urllib.parse 6 from lxml import etre ...

  4. [Azure Devops] 使用 Azure Boards 管理工作

    1. 什么是 Azure Boards 通过 Azure Boards 网络服务,团队可以管理其软件项目.它提供了丰富的功能,包括 Scrum 和看板的本地支持.可定制的仪表板和集成报告.这些工具可以 ...

  5. 对控制器类型“StudentController”的操作“Edit”的当前请求在下列操作方法之间不明确:

    "/"应用程序中的服务器错误. 对控制器类型"StudentController"的操作"Edit"的当前请求在下列操作方法之间不明确:类型 ...

  6. windows与远程linux服务器进行文件传输

    在学习pwn时找到了http://pwnable.kr这个网站,很多题目通过ssh连接, ssh otp@pwnable.kr -p2222 (pw:guest) 连接脚本: pwn_ssh=ssh( ...

  7. golang 性能调优分析工具 pprof(下)

    golang 性能调优分析工具 pprof(上)篇, 这是下篇. 四.net/http/pprof 4.1 代码例子 1 go version go1.13.9 把上面的程序例子稍微改动下,命名为 d ...

  8. SqlServer触发器的创建与使用

    前言 上期我们介绍了SqlServer的视图和存储过程创建与使用,这期我们介绍一下触发器. 有需要回顾的可以电梯直达看一下: SqlServer视图的创建与使用 SqlServer存储过程的创建与使用 ...

  9. Elasticsearch 主节点和暖热节点解析

    Elasticsearch 主节点和暖热节点解析 主节点 控制整个集群,进行一些轻量级操作,列如:跟踪哪些节点是集群中的一部分,决定节点分片分配,负责集群健康, 不包含数据,也不参与搜索和索引操作,对 ...

  10. ffmpeg第五篇:让水印图片旋转起来

    这篇把上次挖的坑填上 ffmpeg正式篇的上一篇(传送门)说了,这一篇要让水印旋转起来,但是后面有事情一直没有时间搞,今天,它来了............ 如果想实现旋转的功能,需要使用ffmpeg过 ...