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【笔记】的更多相关文章

  1. Java开发笔记(六十六)映射:HashMap和TreeMap

    前面介绍了两种集合的用法,它们的共性为每个元素都是唯一的,区别在于一个无序一个有序.虽说往集合里面保存数据还算容易,但要从集合中取出数据就没那么方便了,因为集合居然不提供get方法,没有get方法怎么 ...

  2. Java学习笔记(二二)——Java HashMap

    [前面的话] 早上起来好瞌睡哈,最近要注意一样作息状态.       HashMap好好学习一下. [定义] Hashmap:是一个散列表,它存储的内容是键值对(key——value)映射.允许nul ...

  3. Java HashMap学习笔记

    1.HashMap数据结构 在java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外.HashMap实际 ...

  4. 《Java学习笔记(第8版)》学习指导

    <Java学习笔记(第8版)>学习指导 目录 图书简况 学习指导 第一章 Java平台概论 第二章 从JDK到IDE 第三章 基础语法 第四章 认识对象 第五章 对象封装 第六章 继承与多 ...

  5. 20145330第五周《Java学习笔记》

    20145330第五周<Java学习笔记> 这一周又是紧张的一周. 语法与继承架构 Java中所有错误都会打包为对象可以尝试try.catch代表错误的对象后做一些处理. 使用try.ca ...

  6. 【转】Java HashMap工作原理(好文章)

    大部分Java开发者都在使用Map,特别是HashMap.HashMap是一种简单但强大的方式去存储和获取数据.但有多少开发者知道HashMap内部如何工作呢?几天前,我阅读了java.util.Ha ...

  7. 【转】Java HashMap 源码解析(好文章)

    ­ .fluid-width-video-wrapper { width: 100%; position: relative; padding: 0; } .fluid-width-video-wra ...

  8. Java学习笔记——动态代理

    所谓动态,也就是说这个东西是可变的,或者说不是一生下来就有的.提到动态就不得不说静态,静态代理,个人觉得是指一个代理在程序中是事先写好的,不能变的,就像上一篇"Java学习笔记——RMI&q ...

  9. Java学习笔记4

    Java学习笔记4 1. JDK.JRE和JVM分别是什么,区别是什么? 答: ①.JDK 是整个Java的核心,包括了Java运行环境.Java工具和Java基础类库. ②.JRE(Java Run ...

随机推荐

  1. CentOS-自定义SFTP用户及目录

    ftp功能说明:通过SSH启动CentOS的sftp功能 创建用户组及用户(sftp可变) $ groupadd sftp $ useradd -g sftp -s /sbin/nologin -d ...

  2. python之struct详解

    python之struct详解 2018-05-23 18:20:29 醉小义 阅读数 20115更多 分类专栏: python   版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议 ...

  3. php 写入文件 读取文件内容

    1.写入文件 fopen("文件名.扩展名","操作方式") fwrite(读取的文件,"写入的文件"); fclose(打开的对象变量); ...

  4. Java 在Word中创建邮件合并模板并合并文本和图片

    Word里面的邮件合并功能是一种可以快速批量操作同类型数据的方式,常见的如数据填充.打印等.其中必不可少的步骤包括用于填充的模板文档.填充的数据源以及实现邮件合并的功能.下面,通过Java程序展示如何 ...

  5. chage 修改用户密码时间限制

    chage [options] LOGIN chage针对用户的密码过期时间.过期提前多少天警示等功能实现,passwd也可以实现,但是passwd --expire参数是直接用户密码过期,强制用户下 ...

  6. Grafana、Prometheus-监控平台

    一:Grafana 简介与部署 安利一个生产环境正在使用的监控和告警平台:grafana,它是一个开源的可对指标和日志进行查询.可视化和告警的平台. docker 安装官方文档:https://gra ...

  7. 记录Jackson和Lombok的坑

    记录Jackson和Lombok的坑 今天遇到Jackson反序列化json缺少了字段,后来研究下发现是Jackson的机制和Lombok生成的setter不一致,导致没有正确调用setter. 复现 ...

  8. Scala学习——模式匹配

    scala模式匹配 1.基础match case(类似java里switch case,但功能强大些) object MatchApp { def main(args: Array[String]): ...

  9. 优化 Workerman 检查主进程是否存活的逻辑

    主要新增了判断进程是否为 Workerman 进程的逻辑,从而优化了确定主进程是否存活的准确性 发现问题 年前逛 GitHub 的时候,发现 Workerman 有一个 2017 年打开的 Issue ...

  10. spring boot(二)整合mybatis plus+ 分页插件 + 代码生成

    先创建spring boot项目,不知道怎么创建项目的 可以看我上一篇文章 用到的环境 JDK8 .maven.lombok.mysql 5.7 swagger 是为了方便接口测试 一.Spring ...