Java HashMap【笔记】
Java HashMap【笔记】
HashMap
HashMap 基本结构
HashMap 底层的数据结构主要是数组 + 链表 + 红黑树
其中当链表的长度大于等于 8 时,链表会转化成红黑树,当红黑树的大小小于等于 6 时,红黑树会转化成链表
类注释
1.允许 null 值,不同于 HashTable ,是线程不安全的
2.load factor 默认值是 0.75,是均衡了时间和空间损耗算出来的值,较高的值会减少空间开销,但增加了查找成本,不扩容的条件:数组容量大于需要的数组大小
3.如果有很多数据需要储存到 HashMap 中,那么建议 HashMap 的容量一开始就设置成足够的大小,这样可以防止在其过程中不断的扩容,影响性能
4.HashMap 是非线程安全的,可以通过在外部加锁,或者通过 Collections#synchronizedMap 来实现线程安全,Collections#synchronizedMap 的实现就是在每个方法上加上了 synchronized 锁
5.在迭代过程中,如果 HashMap 的结构被修改,会快速失败
基本属性
**初始容量DEFAULT_INITIAL_CAPACITY **
最大容量MAXIMUM_CAPACITY
**负载因子DEFAULT_LOAD_FACTOR **
链表长度TREEIFY_THRESHOLD
红黑树大小UNTREEIFY_THRESHOLD
数组容量MIN_TREEIFY_CAPACITY
HashMap新增
新增key,value(put(key,value))的步骤如下:
1.首先看一下空数组有没有初始化,如果没有的话就先初始化
2.如果可以通过 key 的 hash 直接找到值,那就直接跳转到第6步,要不就进行到第3步
3.看一下是什么情况,如果是 hash 冲突,进行解决,解决方案有两种,链表或者红黑树
4.如果是链表的话,就进行递归循环,把新元素追加到队尾
5.如果是红黑树的话,就调用红黑树新增的方法
6.通过第二步第四步以及第五步,就可以把新元素追加成功,然后再根据 onlyIfAbsent 判断是否需要覆盖
7.最后判断是否需要扩容,如果需要扩容的话,就进行扩容
具体流程怎么个说呢,将参数,数组长度,数组索引下标啥的都设好以后,开始进行,如果数组为空,那么就是用resize方法进行初始化,如果当前的索引位置是空的,那就在当前的索引位置上直接生成新的节点,那么如果当前索引位置上有值的处理方法,我们就要看key的hash和值是不是都相等,如果都相等,那么就直接把当前下标位置的node值赋值给临时变量,如果是红黑树,就使用红黑树的方法进行新增,如果是个链表,就使用链表的新增方法,然后看一下是不是需要覆盖,只有在onlyIfAbsent 为 false 时,才会覆盖,然后记录一下HashMap的数据结构发生了变化,如果HashMap的实际大小大于扩容的门槛儿,就开始扩容,否则就结束
代码流程:
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;
if ((p = tab[i = (n - 1) & hash]) == 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;
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)
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
链表的新增节点
链表的新增就是将当前节点追加到链表的尾部,和 LinkedList 的追加实现基本一样,从头开始遍历链表,当遍历到链表尾部时,把新节点放到链表尾部,链表遍历过程中,发现有元素和新增的元素相等,结束循环
需要注意的是,当链表长度大于等于 8 时,此时的链表就会转化成红黑树,链表转化红黑树的方法是:treeifyBin,此方法有一个判断,当链表长度大于等于 8,并且整个数组大小大于 64 时,才会转成红黑树,当数组大小小于 64 时,只会触发扩容,不会转化成红黑树
为什么链表的长度大于等于8就会有这种转化?
简单来说,就是在链表查询的时间复杂度,红黑树的时间复杂度以及泊松分布概率函数的综合考虑下,选取出一个边界值,需要在正常情况下不太可能出现的情况发生时进行性能的保证,如果真的出现了,那么可能就是hash算法出了问题,为了保持高性能,就需要转化红黑树
红黑树的新增节点
首先明确红黑树的原则:
1.节点是红色或黑色
2.根是黑色
3.所有叶子都是黑色
4.从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点
5.从每个叶子到根的所有路径上不能有两个连续的红色节点
过程如下:
1.首先判断新增的节点在红黑树上是不是已经存在,如果节点没有实现 Comparable 接口,使用 equals 进行判断,如果节点自己实现了 Comparable 接口,使用 compareTo 进行判断
2.新增的节点如果已经在红黑树上,直接返回,不在的话,判断新增节点是在当前节点的左边还是右边,左边值小,右边值大
3.自旋递归第一步和第二步,直到当前节点的左边或者右边的节点为空时,停止自旋,当前节点即为我们新增节点的父节点
4.把新增节点放到当前节点的左边或右边为空的地方,并于当前节点建立父子节点关系
5.进行着色和旋转,结束
红黑树的着色(给红黑树的节点上色)或旋转(让红黑树更加平衡)的情况:
着色:新节点总是为红色;如果新节点的父亲是黑色,则不需要重新着色,如果父亲是红色,那么必须通过重新着色或者旋转的方法,再次达到红黑树的原则
旋转: 父亲是红色,叔叔是黑色时,进行旋转,如果当前节点是父亲的右节点,则进行左旋,如果当前节点是父亲的左节点,则进行右旋
HashMap查找
HashMap 的查找主要分为以下三步:
第一步,根据 hash 算法定位数组的索引位置,equals 判断当前节点是否是我们需要寻找的 key,是的话直接返回,不是的话继续进行
第二步,判断当前节点有无 next 节点,有的话就进行判断,看一下是链表类型还是红黑树类型
第三步,分别走链表和红黑树不同类型的查找方法
链表查找的话,采用自旋的方式从链表中查找key,如果当前的节点hash等于key的hash,并且equals相等,那么当前的节点就是我们需要的节点,当hash冲突的时候,同一个hash值上是一个链表的时候,我们可以通过equals的方法来比较key是不是相等的,如果这个节点不是我们需要的,那么就把当前节点的下一个节点拿出来继续寻找
代码:
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
红黑树的查找思路,先从根节点进行递归查找,然后根据hashcode来比较查找节点,左边的节点,右边节点的大小,根据红黑树的左边值小,右边值大来进行判断,然后判断查找节点有没有定位节点位置,有的话就返回,没有的话就重复上面操作,一直到自旋到定位节点位置为止
一些问题:
Map 的 hash 算法是怎么进行的?
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
如上代码是 HashMap 的hash 算法
说白了就是一个数学问题,源码中就是这样计算 hash 的,首先计算出 key 的 hashcode,因为 key 是 Object,所以会根据 key 的不同类型进行 hashcode 的计算,接着计算 h ^ (h >>> 16)
好处:大多数场景下,算出来的 hash 值比较分散
一般来说,hash 值算出来之后,要计算当前 key 在数组中的索引下标位置时,可以采用取模的方式,就是索引下标位置 = hash 值 % 数组大小
好处:可以保证计算出来的索引下标值可以均匀的分布在数组的各个索引位置上
此问题可以延伸出三个小问题:
1:为什么不用 key % 数组大小,而是需要用 key 的 hash 值 % 数组大小?
key 还有可能是字符串,是复杂对象,这时候用字符串或复杂对象 % 数组大小是肯定不行的,所以需要先计算出 key 的 hash 值
2:计算 hash 值时,为什么需要右移 16 位?
减少了碰撞的可能性
3:为什么把取模操作换成了 & 操作?
处理器对 & 操作就比较擅长,换成了 & 操作,是有数学上的证明的支撑,为了提高了处理器处理的速度
4:为什么提倡数组大小是 2 的幂次方?
因为只有大小是 2 的幂次方时,才能使 hash 值 % n(数组大小) == (n-1) & hash 公式成立。
HashMap 中出现 hash 冲突时怎么办?
简单地说,先试试扩容
hash 冲突指的是 key 值的 hashcode 计算相同,但 key 值不同的情况
如果元素原本只有一个或已经是链表了,新增元素直接追加到链表尾部
如果元素已经是链表,并且链表个数大于等于 8 时,此时有两种情况:如果此时数组大小小于 64,数组再次扩容,链表不会转化成红黑树,如果数组大小大于 64 时,链表就会转化成红黑树
在数组容量小的情况下冲突严重的话,我们可以先尝试扩容,看看能否通过扩容来解决冲突的问题
Java HashMap【笔记】的更多相关文章
- Java开发笔记(六十六)映射:HashMap和TreeMap
前面介绍了两种集合的用法,它们的共性为每个元素都是唯一的,区别在于一个无序一个有序.虽说往集合里面保存数据还算容易,但要从集合中取出数据就没那么方便了,因为集合居然不提供get方法,没有get方法怎么 ...
- Java学习笔记(二二)——Java HashMap
[前面的话] 早上起来好瞌睡哈,最近要注意一样作息状态. HashMap好好学习一下. [定义] Hashmap:是一个散列表,它存储的内容是键值对(key——value)映射.允许nul ...
- Java HashMap学习笔记
1.HashMap数据结构 在java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外.HashMap实际 ...
- 《Java学习笔记(第8版)》学习指导
<Java学习笔记(第8版)>学习指导 目录 图书简况 学习指导 第一章 Java平台概论 第二章 从JDK到IDE 第三章 基础语法 第四章 认识对象 第五章 对象封装 第六章 继承与多 ...
- 20145330第五周《Java学习笔记》
20145330第五周<Java学习笔记> 这一周又是紧张的一周. 语法与继承架构 Java中所有错误都会打包为对象可以尝试try.catch代表错误的对象后做一些处理. 使用try.ca ...
- 【转】Java HashMap工作原理(好文章)
大部分Java开发者都在使用Map,特别是HashMap.HashMap是一种简单但强大的方式去存储和获取数据.但有多少开发者知道HashMap内部如何工作呢?几天前,我阅读了java.util.Ha ...
- 【转】Java HashMap 源码解析(好文章)
.fluid-width-video-wrapper { width: 100%; position: relative; padding: 0; } .fluid-width-video-wra ...
- Java学习笔记——动态代理
所谓动态,也就是说这个东西是可变的,或者说不是一生下来就有的.提到动态就不得不说静态,静态代理,个人觉得是指一个代理在程序中是事先写好的,不能变的,就像上一篇"Java学习笔记——RMI&q ...
- Java学习笔记4
Java学习笔记4 1. JDK.JRE和JVM分别是什么,区别是什么? 答: ①.JDK 是整个Java的核心,包括了Java运行环境.Java工具和Java基础类库. ②.JRE(Java Run ...
随机推荐
- 打通“任督二脉”:Android 应用安装优化实战
疑问: (1)了解APK安装流程有什么好处 (2)了解APK安装流程可以解决什么问题 一.可以在安装流程里做什么 安装就分为下面三个阶段,每个阶段可以做些什么工作,可以帮助我们优化安装流程,解决安装后 ...
- Postman团队协作开发
介绍 Postman是一款强大的API开发调试软件,它跨平台(真正跨平台,支持linux/mac os/windows),同时还提供浏览器插件,可以说是一个良心软件, 今天主要说一下Postman团队 ...
- JS秒表倒计时器 (转)
<html> <body> <span>倒计时30分钟:</span><span id="clock">00:30:00 ...
- Java实验项目二——二维数组实现九九乘法表
Program:打印乘法口诀表 (1)编写一个方法,参数(二维数组),完成将二维数组中的数据按照行列显示的工作. (2)编写一个测试方法,给出99乘法表,放入到二维数组中,调用(1)中的方法,显示乘法 ...
- 安卓开发--探究碎片Fragment
简述: 最近做开发的时候又遇到了Fragment,发现太久没写,都快忘了,就抓紧写个笔记(我太懒的了233) Fragment可以简单的看成迷你的活动,它和活动一样都有布局和生命周期,它可以嵌入活动之 ...
- 全彩LED灯
1.全彩 LED 灯,实质上是一种把红.绿.蓝单色发光体集成到小面积区域中的 LED 灯,控制时对这三种颜色的灯管输出不同的光照强度,即可混合得到不同的颜色,其混色原理与光的三原色混合原理一致.例如, ...
- C++ 继承及委托
从内存角度看继承和多重继承 http://www.doc88.com/p-9075148832569.html 在C++中实现委托(Delegate) https://blog.csdn.net/jf ...
- git rebase 和 git merger
& git merge 在上图中,每一个绿框均代表一个commit.除了c1,每一个commit都有一条有向边指向它在当前branch当中的上一个commit. 图中的项目,在c2之后就开了另 ...
- chage 修改用户密码时间限制
chage [options] LOGIN chage针对用户的密码过期时间.过期提前多少天警示等功能实现,passwd也可以实现,但是passwd --expire参数是直接用户密码过期,强制用户下 ...
- Pandas高级教程之:自定义选项
目录 简介 常用选项 get/set 选项 经常使用的选项 最大展示行数 超出数据展示 最大列的宽度 显示精度 零转换的门槛 列头的对齐方向 简介 pandas有一个option系统可以控制panda ...